I cannot figure out why in the last case is the move constructor called when copy elision is enabled (or even mandatory such as in C++17):
class X {
public:
X(int i) { std::clog << "converting\n"; }
X(const X &) { std::clog << "copy\n"; }
X(X &&) { std::clog << "move\n"; }
};
template <typename T>
X make_X(T&& arg) {
return X(std::forward<T>(arg));
}
int main() {
auto x1 = make_X(1); // 1x converting ctor invoked
auto x2 = X(X(1)); // 1x converting ctor invoked
auto x3 = make_X(X(1)); // 1x converting and 1x move ctor invoked
}
What rules hinder the move constructor to be elided in this case?
UPDATE
Maybe more straightforward cases when move constructors are called:
X x4 = std::forward<X>(X(1));
X x5 = static_cast<X&&>(X(1));
No move constructor is automatically generated.
std::move is actually just a request to move and if the type of the object has not a move constructor/assign-operator defined or generated the move operation will fall back to a copy.
Implicitly-defined move constructor For non-union class types (class and struct), the move constructor performs full member-wise move of the object's bases and non-static members, in their initialization order, using direct initialization with an xvalue argument.
The two cases are subtly different, and it's important to understand why. With the new value semantics in C++17, the basic idea is that we delay the process of turning prvalues into objects as long as possible.
template <typename T>
X make_X(T&& arg) {
return X(std::forward<T>(arg));
}
int main() {
auto x1 = make_X(1);
auto x2 = X(X(1));
auto x3 = make_X(X(1));
}
For x1
, the first expression we have of type X
is the one in the body of make_X
, which is basically return X(1)
. That's a prvalue of type X
. We're initializing the return object of make_X
with that prvalue, and then make_X(1)
is itself a prvalue of type X
, so we're delaying the materialization. Initializing an object of type T
from a prvalue of type T
means directly initializing from the initializer, so auto x1 = make_X(1)
reduces to just X x1(1)
.
For x2
, the reduction is even simpler, we just directly apply the rule.
For x3
, the scenario is different. We have a prvalue of type X
earlier (the X(1)
argument) and that prvalue binds to a reference! At the point of binding, we apply the temporary materialization conversion - which means we actually create a temporary object. That object is then moved into the return object, and we can do prvalue reduction on the subsequent expression all the way. So this reduces to basically:
X __tmp(1);
X x3(std::move(__tmp));
We still have one move, but only one (we can elide chained moves). It's the binding to a reference that necessitates the existence of a separate X
object. The argument arg
and the return object of make_X
must be different objects - which means a move must happen.
For the last two cases:
X x4 = std::forward<X>(X(1));
X x5 = static_cast<X&&>(X(1));
In both cases, we're binding a reference to a prvalue, which again necessitates the temporary materialization conversion. And then in both cases, the initializer is an xvalue, so we don't get the prvalue reduction - we just have move construction from the xvalue that was a materialized temporary object from a prvalue.
Because in the expression X(std::forward<T>(arg))
, even if, in the last case, arg
is a reference bound to a temporary, it is still not a temporary. Inside the function body, the compiler cannot ensure that arg
is not bound to an lvalue. Consider what would happen if the move constructor was elided and you would perform this call:
auto x4 = make_X(std::move(x2));
x4
would become an alias for x2
.
The rules for move elision of the return value is described in [class.copy]/32:
[...]This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):
in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function's return value
when a temporary class object that has not been bound to a reference ([class.temporary]) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move
In the call make_X(X(1))
copy elision actualy happens, but only once:
arg
.X(std::forward<T>(arg))
invokes the move constructor. arg
is not a temporary so the second rule above does not apply.X(std::forward<T>(arg))
should also be moved to construct the return value but this move is elided.About your UPDATE, std::forward
cause materialisation of the temporary X(1)
that is bound to an xvalue: the return of std::forward
. This returned xvalue is not a temporary so copy/elision is not anymore applicable.
Again what would happen in this case if move elision occured. (The c++ grammar is not contextual):
auto x7 = std::forward<X>(std::move(x2));
Nota: After I have seen a new answer about C++17 I wanted to add to confusion.
In C++17, the definition of prvalue
is that changed that there are not any more any move constructor to elide inside your example code. Here example of result code of GCC with the option fno-elide-constructors
in C++14 and then in C++17:
#c++ -std=c++14 -fno-elide-constructors | #c++ -std=c++17 -fno-elide-constructors
main: | main:
sub rsp, 24 | sub rsp, 24
mov esi, 1 | mov esi, 1
lea rdi, [rsp+15] | lea rdi, [rsp+12]
call X::X(int) | call X::X(int)
lea rsi, [rsp+15] | lea rdi, [rsp+13]
lea rdi, [rsp+14] | mov esi, 1
call X::X(X&&) | call X::X(int)
lea rsi, [rsp+14] | lea rdi, [rsp+15]
lea rdi, [rsp+11] | mov esi, 1
call X::X(X&&) | call X::X(int)
lea rdi, [rsp+14] | lea rsi, [rsp+15]
mov esi, 1 | lea rdi, [rsp+14]
call X::X(int) | call X::X(X&&)
lea rsi, [rsp+14] | xor eax, eax
lea rdi, [rsp+15] | add rsp, 24
call X::X(X&&) | ret
lea rsi, [rsp+15]
lea rdi, [rsp+12]
call X::X(X&&)
lea rdi, [rsp+13]
mov esi, 1
call X::X(int)
lea rsi, [rsp+13]
lea rdi, [rsp+15]
call X::X(X&&)
lea rsi, [rsp+15]
lea rdi, [rsp+14]
call X::X(X&&)
lea rsi, [rsp+14]
lea rdi, [rsp+15]
call X::X(X&&)
xor eax, eax
add rsp, 24
ret
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