I'd like to understand copy elision rules (if these are the one that apply) in a conversion sequence, during an object direct initialization.
#include <cstdio>
class To {
public:
To() { std::puts("To default constructor"); }
explicit To(int Input) : val(Input) { std::puts("To value constructor"); }
To(To&& object) : val(object.val) { std::puts("To move constructor"); }
~To() { std::puts("To destructor"); }
int val = -1;
};
class From {
public:
explicit From(int Input) : val(Input) {
std::puts("From value constructor");
}
operator To() const {
std::puts("From implicit conversion");
return To{val};
};
~From() { std::puts("From destructor"); }
int val = -1;
};
int main() {
To obj0(To{42}); // 1
std::puts("--------------");
To obj1(From{42}); // 2
std::puts("--------------");
}
Line 1 gives the output:
To value constructor
It seems that copy elision kicks in: despite of its construction side-effects, the temporary To{42}
is not materialized. I think that Prvalue semantics ("guaranteed copy elision") applies. Is it correct?
Yet I would appreciate a clear explanation on why the rvalue materialization is not needed.
Moving on a more complex scenario where a From
object can be converted to a To
one and To
object does not have direct construction from From
object, I'm observing following output:
From value constructor
From implicit conversion
To value constructor
From destructor
At first I would have expected
From value constructor
From implicit conversion
To move constructor
From destructor
But in the light of line 1 I may (temporarily) admit that copy elision takes place to remove the temporary To
but why does not the logic go further and remove also the From
object.
My guess is that in general, the compiler cannot make assumptions on how From
is constructed (maybe it squares the initializer), while with a sequence of To
it can "short circuit" (I'm unclear because these rules are unclear for me). But following this line of reasoning, as the conversion happens, my understanding is that it materializes the temporary To
object and if it is materialized, the guaranteed copy elision rule shouldn't apply anymore?
Thus what are exactly the rules that apply at line 2 and why?
Prior to C++17, an initialization like
To obj0(To{42});
would ask for a temporary object of type To
to be created, and then used to move-initialize obj0
. But the compiler was given leeway to eliminate that temporary by rewriting the initialization to
To obj0{42};
The standard explicitly allowed the compiler to perform this optimization even though it could have a different range of well-defined behaviour from the original program. (Most optimizations are allowed only as long as the resulting optimized program behaves in a way that the original program would have been allowed to.)
In C++17, this rewrite effectively became mandatory: when an object is initialized from a prvalue of the same type (ignoring cv-qualifiers) the compiler must generate code that initializes the object in whatever manner the erstwhile temporary object would have been initialized. The standard no longer even acknowledges any temporary to elide; To{42}
, a prvalue, simply doesn't represent any temporary in the first place. However, there are certain contexts in which a prvalue can be "provoked" into creating a temporary, such as when it is at the top level; To{42};
is a statement that creates a temporary To
object. This is known as materializing the prvalue. A prvalue that is immediately used to initialize a named object of the same type is not materialized.
In the obj1
case, To value constructor
certainly must be printed because you explicitly call that constructor in the following statement:
return To{val};
However, instead of this creating a temporary To
object that is then moved into obj1
, this is what happens instead: the return statement directly calls the value constructor of the final destination, which is obj1
. (The way this works under the hood is that From::operator To
has a hidden extra parameter that is like a To*
, and the return statement initializes the object that that pointer points to.)
However, even though the "copy elision" in the obj1
case is clearly desirable, we currently lack the precise wording in the standard to specify it. This is CWG2327.