Home > Enterprise >  Why is the move ctor not called, in this std::move case?
Why is the move ctor not called, in this std::move case?

Time:11-02

If build and run this short example

#include <memory> // for class template `unique_ptr`
#define LOG() std::printf("[%p] %s\n", this, __PRETTY_FUNCTION__)

class bar_t final
{
public:
    bar_t(int val) : m_val(val) { LOG(); }
    ~bar_t(void) { LOG(); }
    bar_t(bar_t&& dying) : m_val(std::move(dying.m_val)) { LOG(); }
    int get_value(void) const { return m_val; }
private:
    int m_val;
};

class foo_t final
{
public:
    foo_t(int a_val) : m_bar(a_val) { LOG(); }
    ~foo_t(void) { LOG(); }
    bar_t m_bar;
};

std::unique_ptr<foo_t> gen_foo(int val)
{
    return std::make_unique<foo_t>(val);
}
int main(int argc, char *argv[])
{
#if 1
    bar_t&& bar = std::move(gen_foo(42)->m_bar); // Bad
//  bar_t& bar = gen_foo(42)->m_bar; // gives same result as previous line
#else
    bar_t bar(std::move(gen_foo(42)->m_bar)); // Good
#endif
    std::printf("bar.get_value() = %d\n", bar.get_value());
    return 0;
}

We'll have this output

[0x5616d6510e70] bar_t::bar_t(int)
[0x5616d6510e70] foo_t::foo_t(int)
[0x5616d6510e70] foo_t::~foo_t()
[0x5616d6510e70] bar_t::~bar_t()
bar.get_value() = 0

where bar.get_value() returns 0 instead of 42. On the other hand, if we set the #if criterion to 0, build and run again, we'll have

[0x55acef3bfe70] bar_t::bar_t(int)
[0x55acef3bfe70] foo_t::foo_t(int)
[0x7fff70612574] bar_t::bar_t(bar_t&&)
[0x55acef3bfe70] foo_t::~foo_t()
[0x55acef3bfe70] bar_t::~bar_t()
bar.get_value() = 42
[0x7fff70612574] bar_t::~bar_t()

where bar.get_value() returns 42.

The question is why bar.get_value() returns 0 in the first case where the #if criterion is 1? How do we explain it? What happened under the hood that led to 0 instead 42, even though std::move is called to transfer value 42? Thanks.

CodePudding user response:

It is widely recognised that std::move has a misleading name: it does not actually perform a move. Instead, it performs a cast, similar to static_cast<T&&>1.

To perform a move, you need to invoke a move constructor or a move assignment operator. This doesn’t happen in your #if 1 code branch, but it does happen in the other branch, with the explicit constructor call. Since the constructor isn’t marked explicit, you could also have written

bar_t bar = std::move(gen_foo(42)->m_bar);

This looks like the move is performed by std::move but what actually happens is that bar is initialised by calling its move constructor.

Importantly, the reason why your first code does not involve lifetime extension (which would make it work even without the move) is that the temporary object that would need to be bound for lifetime extension to work is the return value of gen_foo(42), not gen_foo(42)->m_bar.

Here’s an example where lifetime extension does work (note that we don’t need std::move since gen_foo(42) is already an rvalue):

std::unique_ptr<foo_t>&& foo = gen_foo(42);  // extends the lifetime
bar_t& bar = foo->m_bar;                     // regular reference

1 The actual implementation is slightly more complex.

  •  Tags:  
  • c
  • Related