Search code examples
c++c++11returncopymove

Why is the copy constructor called instead of the move constructor when returning?


Let’s say I have the class MyClass with a correct move constructor and whose copy constructor is deleted. Now I am returning this class like this:

MyClass func()
{
    return MyClass();
}

In this case the move constructor gets called when returning the class object and everything works as expected.

Now let’s say MyClass has an implementation of the << operator:

MyClass& operator<<(MyClass& target, const int& source)
{
    target.add(source);
    return target;
}

When I change the code above:

MyClass func()
{
    return MyClass() << 5;
}

I get the compiler error, that the copy constructor cannot be accessed because it is deleted. But why is the copy constructor being used at all in this case?


Solution

  • Now I am returning this class via lvalue like this:

    MyClass func()
    {
        return MyClass();
    }
    

    No, the returned expression is an xvalue (a kind of rvalue), used to initialise the result for return-by-value (things are a little more complicated since C++17, but this is still the gist of it; besides, you're on C++11).

    In this case the move constructor gets called when returning the class object and everything works as expected.

    Indeed; an rvalue will initialise an rvalue reference and thus the whole thing can match move constructors.

    When I change the code above:

    … now the expression is MyClass() << 5, which has type MyClass&. This is never an rvalue. It's an lvalue. It's an expression that refers to an existing object.

    So, without an explicit std::move, that'll be used to copy-initialise the result. And, since your copy constructor is deleted, that can't work.


    I'm surprised the example compiles at all, since a temporary can't be used to initialise an lvalue reference (your operator's first argument), though some toolchains (MSVS) are known to accept this as an extension.


    then would return std::move(MyClass() << 5); work?

    Yes, I believe so.

    However that is very strange to look at, and makes the reader double-check to ensure there are no dangling references. This suggests there's a better way to accomplish this that results in clearer code:

    MyClass func()
    {
        MyClass m;
        m << 5;
        return m;
    }
    

    Now you're still getting a move (because that's a special rule when returning local variables) without any strange antics. And, as a bonus, the << call is completely standard-compliant.