Search code examples
c++c++11movervalue-reference

Calling std::move on a return value - what should the signature be


Consider

class X
{
public:
    std::unique_ptr<int> m_sp;
    A m_a;

    A test1()
    {
        return std::move(m_a);
    }

    A&& test2()
    {
        return std::move(m_a);
    } 

    std::unique_ptr<int> test3()
    {
        return std::move(m_sp);
    }

    std::unique_ptr<int>&& test4()
    {
        return std::move(m_sp);
    }

    std::unique_ptr<int> test5()
    {
        return std::make_unique<int>(50);
    }
};

class A
{
public:
    A()
    {
        m_i = 1;
    }

    A(A&& other)
    {
        this->m_i = other.m_i;
        other.m_i = -1;
    }

    A& operator=(A&& other)
    {
        this->m_i = other.m_i;
        other.m_i = -1;
        return *this;
    }

    int m_i;
};

To exercise these classes

X x;
A y;
y.m_i = 10;
y = x.test1();

X x2;
A y2;
y2.m_i = 10;
y2 = x2.test2();

both call A's move assignment but only in the test1 case do we call A's move constructor. Why is that? Is it because as we cannot return a A&& (std::move will cast A to A&&, but test1 says it must return an A).

In general, when one wants to move/transfer ownership of expensive member variables, do you want to specify the return to be an rvalue-reference (A&&) or an lvalue (A) type?

It feels a little unnatural as if you aren't using member variables, you let RVO/NRVO do it's thing and just return an lvalue. Take in the case of unique_ptr, when you've got an automatic variable you have a signature like test5(), but if you have a variable, not suitable for RVO/NRVO, like a member varaible should test3 or test4's signature be preferred.

Interested to know.

Thanks


Solution

  • There's a semantic difference there. When you return an object like in

    A test1()
    {
        return std::move(m_a);
    }
    
    std::unique_ptr<int> test3()
    {
        return std::move(m_sp);
    }
    

    then you always move out of your member. No matter whether the caller does something with the return value or not, you will have moved out of your X into a temporary. Ownership lies no longer with you. The caller may take over the return value. If the caller ignores the return value, the temporary will be destroyed anyways. If you return an rvalue-reference, on the other hand, like in

    A&& test2()
    {
        return std::move(m_a);
    }
    
    std::unique_ptr<int>&& test4()
    {
        return std::move(m_sp);
    }
    

    you are simply offering the caller the opportunity to move from/take over ownership of the object. If the caller does not perform the move, your X will retain the ownership, the object will not be moved.

    The crucial thing to understand is that, contrary to what the name suggests, std::move() does not actually perform a move. It merely allows that a given object be moved from. The actual move is performed by the move constructor or move assignment operator of the respective type.

    So your answer is: it depends on what you want to express. If you return an object, you're saying "I'm throwing this away, if you want it: it's over there". If you return an rvalue reference, you're saying "that's the thing, now's your chance to take it, otherwise I keep it"…