Are the following 3 ways to define objects identical?

220 views Asked by At

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
2

There are 2 answers

0
Human-Compiler On BEST ANSWER

The three statements are not entirely identical in .

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:

class Person{
public:
    ...
    Person(Person&&) = delete;
    ...
};

will fail to compile on statements like:

Person p = Person{}; // Case 2

Example on compiler explorer

Note: that the above code is completely valid in 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:

Person&& p3_1 = Person{};
const Person& p3_2 = Person{};

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 , is quite different from Person 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:

std::mutex mutex = std::mutex{};

But in C++11 that fails to compile. However, you are free to write:

std::mutex&& mutex = std::mutex{};

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{}

23
einpoklum On

Yes - with respect to how construction will occur, and how the constructed variable behaves; but No with respect to the variable's type.

The compiler does not use assignment in any of these cases, i.e. it just has your program default-construct. You can use this code to verify:

#include <iostream>

struct Person {
    Person& operator=(Person&) {
       std::cout << "Assignment: operator=(Person&)\n";  return *this; 
    }
    Person& operator=(Person&&) { 
       std::cout << "Move assignment: operator=(Person&&)\n";  return *this; 
    }
    Person(const Person&) { std::cout << "Copy ctor: Person(Person&)\n"; }
    Person(Person&&) { std::cout << "Move ctor: Person(Person&&)\n"; }
    Person() { std::cout << "Default ctor: Person()\n"; }
};

int main() {
    std::cout << "P1:\n";
    Person p1{};
    std::cout << "Address of P1: " << &p1 << '\n';
    std::cout << "P2:\n";
    Person p2 = Person{};
    std::cout << "Address of P2: " << &p2 << '\n';
    std::cout << "P3:\n";
    Person&& p3 = Person{};
    std::cout << "Address of P3: " << &p3 << '\n';
}

See it on GodBolt.

The behavior for the third statement was a bit surprising to me; I actually though the compiler might reject it outright. Regardless - Please don't declare rvalue references like that. It's confusing to readers - even to me and almost certainly not what you want to be doing. I was certain that p3 behaves like an rvalue reference; but - that's not actually the case, apparently: Despite having type Person&&, it will behave like an lvalue reference when passed to a function.