In my understanding the following are identical:
Person p{}; // Case 1
Person p = {}; // Case 1.5
I noticed
Person p = Person{}; // Case 2
produces the same tracing output as the Case 1
and Case 1.5
above.
Question 1: Comparing case 2 with either case 1 or case 1.5, is it because of copy elision or something else?
Question 2: What are the differences between the following?
Person p{}; // Case 1
Person p = Person{}; // Case 2
Person&& p = Person{}; // Case 3
The three statements are not entirely identical in c++11.
Case 2 requires move construction prior to C++17
The language requires that a move-constructor exist for code of
X x = X{}
-- otherwise the code will fail to compile.For example, using a
Person
class defined like:will fail to compile on statements like:
Example on compiler explorer
Note: that the above code is completely valid in c++17 and onward due to wording changes that allow for objects to be constructed directly in their destination address even when immovable and uncopyable (this is what people often refer to as "guaranteed copy elision").
Case 3 is a lifetime extension of a temporary
The third case is a construction of a temporary whose lifetime is extended by being bound to an rvalue-reference. Lifetime of temporaries can be extended under certain cases where they are bound to either rvalue references or
const
lvalue references. For example, the following two constructions are equivalent in that they bind to a temporary's lifetime:As far as scoping rules go, this has the same lifetime as any other automatic variable (e.g. it will invoke a destructor at the end of the scope in just the same way as
Person person{}
will). However what can be done with construction, at least in c++11, is quite different fromPerson p2 = Person{}
in that this code will always compile even if a move constructor is not present (since this is a reference binding).For example, lets consider an immovable, uncopyable type like
std::mutex
. In C++17 it's valid to write code of:But in C++11 that fails to compile. However, you are free to write:
Which creates and binds a temporary to a reference whose lifetime will be the same as any scoped variable constructed at that point.
Example on compiler explorer.
Note: Intentionally propagating lifetime of a temporary object is not commonly an intentional thing to do, but back before C++17 this is the only way to achieve an almost-always-auto syntax with immovable objects. For example, the above could be rewritten:
auto&& mutex = std::mutex{}