5

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
Second Person Shooter
  • 14,188
  • 21
  • 90
  • 165
  • Does this answer your question? [What does T&& (double ampersand) mean in C++11?](https://stackoverflow.com/questions/5481539/what-does-t-double-ampersand-mean-in-c11) – Soleil Sep 27 '20 at 17:15
  • 3
    @Soleil-MathieuPrévot: It doesn't answer OP's question. – einpoklum Sep 27 '20 at 17:19

2 Answers2

4

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

Human-Compiler
  • 11,022
  • 1
  • 32
  • 59
1

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.

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • What _does_ the last case do/mean wrt variable type? – user2864740 Sep 27 '20 at 17:34
  • 1
    The third variant is safe, the lifetime of the temporary is extended to the lifetime of `p3` the only difference is the type. It is equal to a r-value parameter bound to a temporary argument i.e. it's l-value. – Quimby Sep 27 '20 at 17:44
  • @G.Sliepen: Yes. Fixed. – einpoklum Sep 27 '20 at 18:21
  • 1
    @NotAZoomedImage: No, because I didn't compile with any optimization; and - no elision is possible, since I added side-effects (`std::cout` ops). – einpoklum Sep 27 '20 at 18:22
  • 1
    Your last statement is very strange. `p3` itself would be a lvalue where used despite being of type `Person &&` at declaration site, it needs to be `std::move`d to be used as a rvalue reference. – bipll Sep 27 '20 at 19:06
  • @bipll: `std::is_same_v` evaluates to true. Can you clarify? – einpoklum Sep 27 '20 at 20:11
  • Clarify what exactly? – bipll Sep 27 '20 at 23:00
  • @bipll: Why you wrote that a `p3` would need to be move()'d to be an rvalue reference. – einpoklum Sep 28 '20 at 09:02
  • 1
    Because `p3` as an expression is itself a lvalue, eh? – bipll Sep 28 '20 at 09:51
  • @bipll: So why is its type `Person&&` then? – einpoklum Sep 28 '20 at 10:13
  • 1
    So why not? Run some tests to see which overload is chosen if you pass it as an argument. – bipll Sep 28 '20 at 10:28
  • @einpoklum `p3` is an lvalue expression with the type `Person &&`, not an rvalue expression with type `Person`. – Caleth Sep 29 '20 at 12:03
  • @Caleth: I was under the (apparently mistaken) impression that the a type can only correspond to a single value category... – einpoklum Sep 29 '20 at 12:53
  • @NotAZoomedImage: Equivalent in some aspects, not equivalent in others - although perhaps with bipll's comment, the equivalence is almost complete. – einpoklum Sep 29 '20 at 22:35
  • @einpoklum If I may, I think that Caleth is referring to the [if-it-has-a-name "rule"](http://thbecker.net/articles/rvalue_references/section_05.html). – Bob__ Sep 29 '20 at 23:46
  • "the variable you declared will be an r-value reference, so if you pass it to some function or assign from it - **a move will occur**" This isn't true at all. Any named reference is an **lvalue reference** even if it's declared an rvalue reference. This is why you must still call `std::move` or `std::forward` when dealing with reference parameters / variables – Human-Compiler Sep 30 '20 at 21:04
  • It's also worth pointing out that this question is tagged `c++11`, but the answer is only true for C++17. In C++11 `p2` requires either a move or copy constructor to be defined -- otherwise it will fail to compile – Human-Compiler Sep 30 '20 at 21:07
  • @Human-Compiler: If I change to C++11 at the GodBolt link, it still compiles and runs fine. – einpoklum Sep 30 '20 at 23:00
  • @Human-Compiler: But have correct the text regarding the behavior of `p3`. – einpoklum Sep 30 '20 at 23:02
  • @einpoklum If this were C++11 and the **move constructor were `delete`d**, then the behavior is different. Writing `Person p2 = Person{}` requires a move constructor ([example](https://godbolt.org/z/rbxhz3)). I'm just suggesting since this is tagged C++11 and is asking specifically about behavioral differences, that the answer should reflect it. – Human-Compiler Oct 01 '20 at 01:37
  • @Human-Compiler: It's weird, that the compiler requires the move ctor, but then allows itself to not actually use it... anyway, OP has not mentioned deleting the move ctor. – einpoklum Oct 01 '20 at 06:55
  • To be fair enough (from my point of view of course), a bounty of 100 will be awarded to this answer. – Second Person Shooter Oct 01 '20 at 13:30
  • @NotAZoomedImage: I don't feel I deserve it actually - I learned a bunch from the comments myself. – einpoklum Oct 01 '20 at 15:21