c++movecopy-constructormove-constructor

Returning an object with only explicit move constructor


The following code would not compile:

#include <utility>

class Foo{
public:
    Foo(const Foo& foo) = delete; 
    explicit Foo(Foo&& foo) = default;
    Foo() = default;
    Foo bar(){ 
        Foo foo;
        return foo;
    }
};

with the compiler message of:

test_rand.cpp:10:16: error: use of deleted function ‘Foo::Foo(const Foo&)’
   10 |         return foo;
      |                ^~~
test_rand.cpp:5:5: note: declared here
    5 |     Foo(const Foo& foo) = delete;
      |     ^~~

Thinking that this is because the copy constructor is deleted, and when the function return a temporary variable needs to be created, I added std::move to make foo a rvalue so that the move constructor can be called.

#include <utility>

class Foo{
public:
    Foo(const Foo& foo) = delete; 
    explicit Foo(Foo&& foo) = default;
    Foo() = default;
    Foo bar(){ 
        Foo foo;
        return std::move(foo);
    }
};

However, the compiler gives me the exact same error "use of deleted function 'Foo::Foo(const Foo&)'."

I then tried to remove the explicit keyword for the move constructor, and everything worked, even without the std::move

I wonder what the internal mechanism is for this. Specifically, what are the detailed steps for the compiler to return that value with only a move constructor, and what implicit conversions happen in the return process?

With the explicit keyword kept, I also found that if I changed the return line to return Foo(std::move(foo)), the error disappeared. But what is the difference between this and return std::move(foo), considering both of them are rvalues. And if I want to keep the move constructor explicit, is there a better way of doing so?


Solution

  • The result object of a function call is initialized by copy-initialization from the operand of the return statement. That's the same initialization that you would have e.g. for a function parameter or for initialization with = initializer syntax.

    If the operand is a xvalue, such as std::move(tmp), but in a return statement also just tmp, then copy-initialization will result in a call to the copy constructor, because copy-initialization generally does not consider explicit constructors, just the same as in

    Foo a;
    Foo b = std::move(a);
    

    or

    void f(Foo);    
    
    Foo a;
    f(std::move(a));
    

    If however the return statement's operand is a prvalue such as Foo(std::move(tmp)), then copy-initialization means that the object will be initialized from the initializer of the prvalue. (So-called "mandatory copy elision".) The initialization of the prvalue Foo(std::move(tmp)) is direct-initialization. So the result object of the function call will be initialized by direct-initialization from the argument list (std::move(tmp)). That's the difference to earlier where it was copy-initialized from std::move(tmp).

    In direct-initialization all constructors are considered against the argument list and so the explicit move constructor may be chosen. In this case std::move(tmp) is also required, because tmp is only automatically a xvalue in a return statement if it is the whole operand.

    That's the same behavior as e.g. in

    Foo a;
    Foo b = Foo(std::move(a));
    

    or

    void f(Foo);    
    
    Foo a;
    f(Foo(std::move(a)));