struct Test {
int field = 30;
Test() { cout << "In ctor" << endl; }
Test(const Test &other) { field = other.field; cout << "In copy ctor" << endl; }
Test(Test &&other) { field = other.field; cout << "In move ctor" << endl; }
Test &operator=(const Test &other) { field = other.field; cout << "In copy assignment" << endl; return *this; }
Test &operator=(Test &&other) { field = other.field; cout << "In move assignment" << endl; return *this; }
~Test() { cout << "In dtor" << endl; }
};
Test get_test() {
Test t;
return t;
}
int main() {
Test t2 = get_test();
}
I think this is the canonical NRVO example. I'm compiling with -fno-elide-constructors
and I see that the following are called: ctor, move ctor, dtor, dtor.
So the first ctor call corresponds to the line Test t;
, the move ctor is constructing the t2
in main
, then the temporary that is returned from get_test
is destroyed, then the t2
in main
is destroyed.
What I don't understand is: shouldn't there be a copy ctor invocation when returning by value? That is, I thought that get_test
should be making a copy of t
and then this copy is moved into t2
. It seems like t
is moved into t2
right away.
Starting from C++17, there is mandatory copy elison which says:
Under the following circumstances, the compilers are required to omit the copy and move construction of class objects, even if the copy/move constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. The copy/move constructors need not be present or accessible:
- In the initialization of an object, when the initializer expression is a prvalue of the same class type (ignoring cv-qualification) as the variable type.
- Initializing the returned object in a return statement, when the operand is a prvalue of the same class type (ignoring cv-qualification) as the function return type:
T f() { return U(); // constructs a temporary of type U, // then initializes the returned T from the temporary } T g() { return T(); // constructs the returned T directly; no move }
(emphasis mine)
This means that t2
is constructed directly from the prvalue
that get_test
returns. And since a prvalue
is used to construct t2
, the move constructor is used. Note that in C++17, the flag -fno-elide-constructors
have no effect on return value optimization(RVO) and is different from NRVO.
But prior to C++17, there was non-mandatory copy elison and since you've provided the -fno-elide-constructors
flag, a temporary prvalue
is returned by get_test
using the move constructor. So you see the first call to the move ctor. Then, that temporary is used to initialize t2
again using the move constructor and hence we get the second call to the move ctor.