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