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