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("--------------");
}
LIVE
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?
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("--------------");
}
LIVE
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?
Share Improve this question asked Feb 5 at 15:05 OerstedOersted 2,6046 silver badges25 bronze badges 2 |2 Answers
Reset to default 3Prior 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.
It doesn’t make sense for the To
move constructor to come first in any sequence that doesn’t start from an existing To
object, because what would its parameter be?
“Guaranteed copy elision” (as your quotes perhaps already imply) is a name of only historical relevance: there was once an optional “optimization” called copy elision, and it stopped being optional and just became part of the language. (“Optimization” also gets quotes because, unlike true optimization, it changed the meaning of the program.) The way to think about it is that a prvalue is an initialization in search of an object; the initialization happens “once” it finds out what object it’s initializing (but this is of course determined at compile time).
In your first example, the “temporary expression” To{42}
is a prvalue that says how to initialize some To
object, and it turns out that that is obj0
. In the second example, of course the output from From
cannot be elided: your program creates and converts such an object, so of course the side effects of those operations are visible. (If you compile with optimizations, you’ll see that the resulting assembly does nothing but that output, since none of the “real” values are ever used.)
This is one of the advantages of the “new” (C++17) rules: there are fewer cases where the implementation is allowed to omit apparent side effects, so we needn’t be on the lookout for them around every corner.
return To{val}
. Move constructor is elided; theTo
object is constructed directly in the storage reserved forobj1
. The construction fromint
toTo
must happen somewhere - how else would anint
value turn into aTo
object? – Igor Tandetnik Commented Feb 5 at 21:54From
object". Copy elision may elide copy and move constructors (and corresponding destructors, of course). It cannot elideFrom(int)
constructor. – Igor Tandetnik Commented Feb 5 at 21:56