How do I avoid the move constructor in debug macros?

I am trying to write a debug macro / template that prints out the function name followed by the return value and then returns that value. I came up with 4 macros: RETURN

for basic types, RETURN_P

for std-wrapped pointers (uniqe_ptr, shared_ptr, optional), RETURN_B

for booleans and RETURN_A

for things that I can only show the address.

To test macros, I wrote a class Resource

that is movable but not copyable and has a "private" constructor. I use the private access argument so that no one calls the constructor while still being able to use forwarding functions such as std::make_unique

. This is a test object, so it provides factories for all the types I've encountered so far. For each type there is a factory that creates that type as part of the makro intro and a factory that first creates a temporary variable and then calls the macro with it.

The latter function main()

creates many objects Resource

that test all macros, including the basic int and bool types.

Now here are my problems:

  • Using the NDEBUG methods, alloc_opt2

    and create2

    call the resource move constructor. Can this be avoided in some way?
  • When NDEBUG is not defined, the alloc_opt

    and methods create

    also refer to the Resource move constructor. I really want at least these steps to go away so the behavior is the same with and without NDEBUG.

Is there something that can be done?

#include <memory>
#include <tuple>
#include <experimental/optional>
#include <cassert>
#include <stdio.h>

class Resource {
    struct access { };
public:
    static Resource * alloc_ptr();
    static Resource * alloc_ptr2();
    static std::unique_ptr<Resource> alloc_uniqe();
    static std::unique_ptr<Resource> alloc_uniqe2();
    static std::shared_ptr<Resource> alloc_shared();
    static std::shared_ptr<Resource> alloc_shared2();
    static std::experimental::optional<Resource> alloc_opt();
    static std::experimental::optional<Resource> alloc_opt2();
    static Resource create();
    static Resource create2();
    Resource(access);
    ~Resource();
    Resource(Resource &&other);
    operator bool() const;
private:
    Resource(const Resource &) = delete;
    Resource & operator =(const Resource &) = delete;
    bool valid;
};

#define NDEBUG

#ifdef NDEBUG
#define RETURN(format, arg) return arg;
#define RETURN_P(arg) return arg;
#define RETURN_B(arg) return arg;
#define RETURN_A(arg) return arg;
#else

template <class T>
T debug(const char *format, const char *name, T && t) {
    fprintf(stderr, "[<T>] ");
    fprintf(stderr, format, name, t);
    return std::forward<T>(t);
}

template <class T>
T debug_p(const char *format, const char *name, T && t) {
    fprintf(stderr, "[ptr<T>] ");
    fprintf(stderr, format, name, t ? &*t : nullptr);
    return std::forward<T>(t);
}

bool debug_b(const char *format, const char *name, bool t) {
    fprintf(stderr, "[bool] ");
    fprintf(stderr, format, name, t ? "true" : "false");
    return t;
}

template <class T>
T debug_a(const char *format, const char *name, T && t) {
    fprintf(stderr, "[&<T>] ");
    fprintf(stderr, format, name, &t);
    return std::forward<T>(t);
}

#define RETURN(format, arg) return debug("%s : " format "\n", __PRETTY_FUNCTION__, arg);
#define RETURN_P(arg) return debug_p("%s = %p\n", __PRETTY_FUNCTION__, arg);
#define RETURN_B(arg) return debug_b("%s = %s\n", __PRETTY_FUNCTION__, arg);
#define RETURN_A(arg) return debug_a("%s = %p\n", __PRETTY_FUNCTION__, arg);

#endif

Resource * Resource::alloc_ptr() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    RETURN_P(new Resource(access{}));
}

Resource * Resource::alloc_ptr2() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    Resource * r = new Resource(access{});
    RETURN_P(r);
}

std::unique_ptr<Resource> Resource::alloc_uniqe() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    RETURN_P(std::make_unique<Resource>(access{}));
}

std::unique_ptr<Resource> Resource::alloc_uniqe2() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    std::unique_ptr<Resource> r = std::make_unique<Resource>(access{});
    RETURN_P(std::move(r));
}

std::shared_ptr<Resource> Resource::alloc_shared() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    RETURN_P(std::make_shared<Resource>(access{}));
}

std::shared_ptr<Resource> Resource::alloc_shared2() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    std::shared_ptr<Resource>  r = std::make_shared<Resource>(access{});
    RETURN_P(r);
}

std::experimental::optional<Resource> Resource::alloc_opt() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    RETURN_P(std::experimental::optional<Resource>(std::experimental::in_place, access{}));
}

std::experimental::optional<Resource> Resource::alloc_opt2() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    std::experimental::optional<Resource>  r = std::experimental::optional<Resource>(std::experimental::in_place, access{});
    RETURN_P(std::move(r));
}

Resource Resource::create() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    RETURN_A(Resource(access{}));
}

Resource Resource::create2() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    Resource r = Resource(access{});
    RETURN_A(std::move(r));
}

Resource::Resource(access) : valid(true) {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
}

Resource::~Resource() {
    fprintf(stderr, "%s [%s]\n", __PRETTY_FUNCTION__,
            valid ? "valid" : "invalid");
    valid = false;
}

Resource::Resource(Resource &&other) : valid(other.valid) {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    assert(other.valid);
    other.valid = false;
}

Resource::operator bool() const {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    RETURN_B(valid);
}

int test_int() {
    fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
    RETURN("%d", 1);
}

int main() {
    fprintf(stderr, "### test test_int()\n");
    {
    int i = test_int();
    fprintf(stderr, "### test test_int() created\n");
    if (i) { }
    }
    fprintf(stderr, "### test test_int() done\n\n");

    fprintf(stderr, "### test Resource::alloc_ptr()\n");
    {
    Resource * r1 = Resource::alloc_ptr();
    fprintf(stderr, "### test Resource::alloc_ptr() allocated\n");
    delete r1;
    }
    fprintf(stderr, "### test Resource::alloc_ptr() done\n\n");

    fprintf(stderr, "### test Resource::alloc_ptr2()\n");
    {
    Resource * r1 = Resource::alloc_ptr2();
    fprintf(stderr, "### test Resource::alloc_ptr2() allocated\n");
    delete r1;
    }
    fprintf(stderr, "### test Resource::alloc_ptr2() done\n\n");

    fprintf(stderr, "### test Resource::alloc_unique()\n");
    {
    std::unique_ptr<Resource> r2 = Resource::alloc_uniqe();

    fprintf(stderr, "### test Resource::alloc_uniqe() allocated\n");
    if (r2) { }
    }
    fprintf(stderr, "### test Resource::alloc_uniqe() done\n\n");

    fprintf(stderr, "### test Resource::alloc_unique2()\n");
    {
    std::unique_ptr<Resource> r2 = Resource::alloc_uniqe2();

    fprintf(stderr, "### test Resource::alloc_uniqe2() allocated\n");
    if (r2) { }
    }
    fprintf(stderr, "### test Resource::alloc_uniqe2() done\n\n");

    fprintf(stderr, "### test Resource::alloc_shared()\n");
    {
    std::shared_ptr<Resource> r3 = Resource::alloc_shared();
    fprintf(stderr, "### test Resource::alloc_shared() allocated\n");
    if (r3) { }
    }
    fprintf(stderr, "### test Resource::alloc_shared() done\n\n");

    fprintf(stderr, "### test Resource::alloc_shared2()\n");
    {
    std::shared_ptr<Resource> r3 = Resource::alloc_shared2();
    fprintf(stderr, "### test Resource::alloc_shared2() allocated\n");
    if (r3) { }
    }
    fprintf(stderr, "### test Resource::alloc_shared2() done\n\n");

    fprintf(stderr, "### test Resource::alloc_opt()\n");
    {
    std::experimental::optional<Resource> r4 = Resource::alloc_opt();
    fprintf(stderr, "### test Resource::alloc_opt() allocated\n");
    if (r4) { }
    }
    fprintf(stderr, "### test Resource::alloc_opt() done\n\n");

    fprintf(stderr, "### test Resource::alloc_opt2()\n");
    {
    std::experimental::optional<Resource> r4 = Resource::alloc_opt2();
    fprintf(stderr, "### test Resource::alloc_opt2() allocated\n");
    if (r4) { }
    }
    fprintf(stderr, "### test Resource::alloc_opt2() done\n\n");

    fprintf(stderr, "### test Resource::create()\n");
    {
    Resource r5(Resource::create());
    fprintf(stderr, "### test Resource::create() created\n");
    if (r5) { }
    }
    fprintf(stderr, "### test Resource::create() done\n\n");

    fprintf(stderr, "### test Resource::create2()\n");
    {
    Resource r5(Resource::create2());
    fprintf(stderr, "### test Resource::create2() created\n");
    if (r5) { }
    }
    fprintf(stderr, "### test Resource::create2() done\n\n");
}

      

+3


source to share


3 answers


In a mode other than NDEBUG is, move the design in create

, create2

, alloc_opt

and alloc_opt2

can not be deleted (if they are not called standard), as an exception is prohibited unless you return parameter of the function. I tried but couldn't find a way to debug the return values ​​transparently, safe to use and not navigate. Combining NRVO with resetting the value before returning it seems like a better idea, but that means, as I said, unload it first and then return it rather than passing it through a debug function.

So you have something like

    Resource r(access{});
    DUMP_RESOURCE(r);  // disappears if NDEBUG
    return r;
}

      

Make sure you don't

  • put a return statement inside a block (like a trick do { } while(0)

    )
  • returns any expression more complex than a variable name (for example, std::move(r)

    or(DUMP(r), r)

(edit 4: An idea was removed here that turned out to be ineffective and explained what worked instead.)

There is also no compiler-independent way to get rid of moves in NDEBUG mode. The C ++ standard explains that reverting to a value creates a move or copy, which can be eliminated to optimize the program except for one special case:

return {access()};

      

In this case, direct construction of the return value is guaranteed.



I would expect a good compiler to remove the hyphenation constructs if optimization is enabled in all cases if NDEBUG is defined. In case create

this is called return value optimization (RVO), and in create2

this case it is called name-return-value optimization (NRVO).

This is not considered good style for std::move

local variables in return statements, and also std::move

blocks NRVOs create2

in g ++ 4.9. I understand that you need std :: move if you are passing this object through debug functions; but here you can follow Jonathan Vakili's advice on setting this std :: move to a debug macro, which seems like a good idea too.

(edit 3: I checked the statement in the last paragraph)


A workaround for getting any object created by a function in the caller's scope without moving is that instead of returning it, you are emplace

in the caller provided by std :: experimental :: optional, which is passed in a disabled state.

void Resource::workaround(std::experimental::optional<Resource> &output)
{
    output.emplace(access{});
#ifndef NDEBUG
    debug_print(*output);
#endif
}

// call site:
int main()
{
    std::experimental::optional<Resource> result;
    Resource::workaround(result);
    // work with "*result" now
}

      

As with return by value, the result is managed on the main stack, but you are guaranteed not to get any move constructs.

(Edit 2: completely reworked workaround added in edit 1)

+1


source


  • The alloc_opt2 and create2 methods are defined with NDEBUG and call the resource move constructor. Can this be avoided in some way?

Yes, it's simple, just stop writing RETURN_X(std::move(r))

and say RETURN_X(arg)

instead where the macro is calling std::move

instead:

#define RETURN_X return debug_x("%s = %p\n", __PRETTY_FUNCTION__, std::move(arg));

      



Now that NDEBUG is defined, the return statements are simple return arg;

, so you get a switch.

When NDEBUG is not defined, you still move the return value into the debug functions as before.

+1


source


Launching a new answer as it is too long to comment:

You can try to get around this along the lines

#define RETURN_A(arg) do { auto t = arg; \
    debug_a("%s = %p\n", __PRETTY_FUNCTION__, &t); \
    return t; } while(0)

      

because in this case you are returning a local variable in which the copies can be deleted. (Edited: renamed x to t, returns t and outputs & t to match the previous one RETURN_A

)

This results in the following error:

debug.cc: In static member function 'static Resource Resource::create2()':
debug.cc:129:14: error: use of deleted function 'Resource::Resource(const Resource&)'
     RETURN_A(r);
              ^
debug.cc:73:37: note: in definition of macro 'RETURN_A'
 #define RETURN_A(arg) do { auto t = arg; fprintf(stderr, "%s = %p\n", __PRETTY_FUNCTION__, &t); return t; } while(0)
                                     ^
debug.cc:25:5: note: declared here
     Resource(const Resource &) = delete;
     ^

      

Neither std::move(t)

, nor std::forward<decltype(t)>(t)

solve it.

0


source







All Articles