Look at this code:
class Foo
{
public:
string name;
Foo(string n) : name{n}
{
cout << "CTOR (" << name << ")" << endl;
}
Foo(Foo&& moved)
{
cout << "MOVE CTOR (moving " << moved.name << " into -> " << name << ")" << endl;
name = moved.name + " ###";
}
~Foo()
{
cout << "DTOR of " << name << endl;
}
};
Foo f()
{
return Foo("Hello");
}
int main()
{
Foo myObject = f();
cout << endl << endl;
cout << "NOW myObject IS EQUAL TO: " << myObject.name;
cout << endl << endl;
return 0;
}
The output is:
[1] CTOR (Hello)
[2] MOVE CTOR (moving Hello into -> )
[3] DTOR of Hello
[4] MOVE CTOR (moving Hello ### into -> )
[5] DTOR of Hello ###
[6] NOW two IS EQUAL TO: Hello ### ###
[7] DTOR of Hello ### ###
Important note: I have disabled the copy elision optimization using -fno-elide-constructors
for testing purposes.
The function f() constructs a temporary [1] and returns it calling the move constructor to "move" the resources from that temporary to myObject [2] (additionally, it adds 3 # symbols).
Eventually, the temporary is destructed [3].
I now expect myObject to be fully constructed and its name attribute to be Hello ###.
Instead, the move constructor gets called AGAIN, so I'm left with Hello ### ###
If a copy constructor, copy-assignment operator, move constructor, move-assignment operator, or destructor is explicitly declared, then: No move constructor is automatically generated. No move-assignment operator is automatically generated.
If any constructor is being called, it means a new object is being created in memory. So, the only difference between a copy constructor and a move constructor is whether the source object that is passed to the constructor will have its member fields copied or moved into the new object.
And there are 4 calls to copy constructor in f function. 1) u is passed by value. 2) v is copy-initialized from u . 3) w is copy-initialized from v . 4) w is copied on return.
By definition an object is only constructed once, hence the constructor is only called once.
The two move constructor calls are:
Foo("Hello")
into the return value.f()
call into myObject
.If you used a braced-init-list to construct the return value, there would only be a single move construction:
Foo f()
{
return {"Hello"};
}
This outputs:
CTOR (Hello)
MOVE CTOR (moving Hello into -> )
DTOR of Hello
NOW myObject IS EQUAL TO: Hello ###
DTOR of Hello ###
Live Demo
Because you turned off copy elision, your object first gets created in f()
, then gets moved into the return value placeholder for f()
. At this point f
's local copy is destroyed. Next the return object is moved into myObject
, and also destroyed. Finally myObject
is destroyed.
If you didn't disable copy elision, you would have seen the sequence you expected.
UPDATE: to address question in comment below, which is - given the definition of a function like this:
Foo f()
{
Foo localObject("Hello");
return localObject;
}
Why is the move constructor invoked in the creation of the return-value object with copy elision disabled? After all, localObject above is an lvalue.
The answer is that the compiler is obliged in these circumstances to treat the local object as an rvalue, so effectively it is implicitly generating the code return std::move(localObject)
. The rule that requires it to do so is in the standard [class.copy/32] (relevant parts highlighted):
When the criteria for elision of a copy/move operation are met, but not for an exception-declaration, and the object to be copied is designated by an lvalue, or when the expression in a return statement is a (possibly parenthesized) id-expression that names an object with automatic storage duration declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue.
...
[ Note: This two-stage overload resolution must be performed regardless of whether copy elision will occur. It determines the constructor to be called if elision is not performed, and the selected constructor must be accessible even if the call is elided. — end note ]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With