ttzytt
ttzytt

Reputation: 101

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?

Upvotes: 0

Views: 146

Answers (3)

user17732522
user17732522

Reputation: 76829

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)));

Upvotes: 4

JaMiT
JaMiT

Reputation: 17073

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.

Both are rvalues, but what do you do with those rvalues? In the first case, you explicitly construct a Foo object from it (then return value optimization kicks in, avoiding the need to construct another object). In the second case, you merely say what to do with the rvalue (return it), with the implication that the returned object be constructed from it (without that implication, you would be returning Foo&& instead of Foo).

So, what you see is the consequence of marking a constructor explicit. An explicit constructor must be invoked explicitly.

You're also seeing why an explicit move constructor is awkward...

Upvotes: 0

3CxEZiVlQ
3CxEZiVlQ

Reputation: 38883

When you call bar(), Foo bar = baz.bar(), and it does return Foo(std::move(foo)), the copy elision makes it equivalent

Foo bar(std::move(foo));  // explicit move constructor.

This is fine. When Foo::bar() does return std::move(foo);, the copy elision does not apply and makes this

Foo tmp(std::move(foo));
Foo bar(tmp);  // copy constructor.

This can't compile do you the deleted copy constructor.

Upvotes: 0

Related Questions