Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why C++ lvalue objects can't be bound to rvalue references (&&)?

The idea of move semantics is that you can grab everything from another temporary object (referenced by an rvalue reference) and store that "everything" in your object. That helps to avoid deep copying where single construction of things is enough -- so you construct things in a rvalue object and then just move it to your long living object.

Why is it that C++ doesn't allow binding lvalue objects to rvalue references? Both allow me to change the referenced object, so there is no difference to me in terms of accessing internals of referenced object.

The only reason I can guess is function overloading ambiguity issues.

like image 207
pavelkolodin Avatar asked Feb 26 '16 13:02

pavelkolodin


People also ask

Can lvalue bind to rvalue reference?

An lvalue reference can bind to an lvalue, but not to an rvalue.

What is the difference between lvalue and rvalue references?

“l-value” refers to a memory location that identifies an object. “r-value” refers to the data value that is stored at some address in memory. References in C++ are nothing but the alternative to the already existing variable. They are declared using the '&' before the name of the variable.

How do rvalue references work?

An rvalue reference is formed by placing an && after some type. An rvalue reference behaves just like an lvalue reference except that it can bind to a temporary (an rvalue), whereas you can not bind a (non const) lvalue reference to an rvalue.

What are L values and R values?

An lvalue (locator value) represents an object that occupies some identifiable location in memory (i.e. has an address). rvalues are defined by exclusion. Every expression is either an lvalue or an rvalue, so, an rvalue is an expression that does not represent an object occupying some identifiable location in memory.


2 Answers

But why C++ doesn't allow binding lvalue objects to rvalue references?

Assuming you mean "Why doesn't C++ allow binding rvalue references to lvalue objects": it does. It just isn't automatic, so you have to use std::move to make it explicit.

Why? Because otherwise an innocuous function call can surprisingly destroy something you didn't expect it to:

Class object(much,state,many,members,wow);
looks_safe_to_me(object);
// oh no, it destructively copied my object!

vs.

Class object(much,state,many,members,wow);
obviously_destructive(std::move(object));
// same result, but now the destruction is explicit and expected

A note on destructive copying: why I say destructively and destruction above, I don't mean the object destructor ends its lifetime: just that its internal state has been moved to a new instance. It's still a valid object, but no longer holds the same expensive state it used to.


A note on terminology: let's see if we can clear up the imprecise use of lvalue, rvalue etc. above.

Quoting from cppreference for posterity:

  • an lvalue is

    an expression that has identity and cannot be moved from.

    So, there's no such thing as an lvalue object, but there is an object which is locally named (or referred to) by an lvalue expression

  • an rvalue is

    an expression that is either a prvalue or an xvalue. It can be moved from. It may or may not have identity.

    • a prvalue (pure rvalue) is roughly an expression referring to an un-named temporary object: we can't convert our lvalue expression to one of these IIUC.

    • an xvalue (expiring value) is

      an expression that has identity and can be moved from.

      which explicitly includes the result of std::move

So what actually happens:

  • an object exists
  • the object is identified locally by an lvalue expression, which cannot be moved from (to protect us from unexpected side-effects)
  • std::move yields an xvalue expression (which can be moved from) referring to the same object as the lvalue expression
  • this means objects such as variables (which are named by lvalue expressions) cannot be implicitly moved from, and must instead explicitly moved from via an explicit xvalue expression such as std::move.
  • anonymous temporaries are probably already referred to by prvalue expressions, and can be moved implicitly
like image 99
Useless Avatar answered Oct 06 '22 15:10

Useless


Essentially, a mechanism is needed to distinguish between values that can be moved from, and those that cannot be moved from (i.e. a copy would be needed).

Allowing both rvalues and lvalues to be bound to an lvalue reference makes that impossible.

Hence, values bound to an rvalue reference can be moved from (not necessarily always going to be moved from, but it is allowed), and lvalues can be bound to lvalue references and can't be moved from.

std::move is there to allow for the casting between the value categories (to an rvalue) to allow the move to happen.

Note; const lvalue references (const T&) can be bound to both rvalues (temporaries) and lvalues since the referred to object can't change (it is marked as const so there can't be any moving from anyway).

There is some history (back to the early days of C++) to why temporary objects could not be bound to non-const lvalue references to begin with... the detail is blurry but it there was some reasoning that modifying a temporary didn't make sense, since it would destruct at the end of the current statement anyway. Additionally you could be lulled into the sense that you were modifying an lvalue, when you where in fact not - the semantics of the code could/would be wrong and be buggy. There are further reasons tied to addresses, literals etc. This was before moving and the semantics thereof solidified, and is some of the motivation for moving and its semantics.

like image 21
Niall Avatar answered Oct 06 '22 16:10

Niall