Reputation: 101
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
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
Reputation: 17073
With the
explicit
keyword kept, I also found that if I changed the return line toreturn Foo(std::move(foo))
, the error disappeared. But what is the difference between this andreturn 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
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