rvalue and lvalue - krishnaramb/cplusplus GitHub Wiki

Contents

Definition

An lvalue (locator value) πŸ‘Šrepresents an object that occupies some identifiable location in memory (i.e. has an address 🌺).

rvalue are defined by exclusion which means every expression is either an lvalue or an rvalue. Therefore, from the above definition of lvalue, an rvalue is an expression that does not represent an object occupying some identifiable location in memory.

  • r-values: can not be on the lhs of assignment
  • r-values: don't have an address
  • l-values: can be on the lhs of assignment
  • l-value: do have an address

lets see one example

int foo() {return 2;}

int main()
{
    foo() = 2;

    return 0;
}

when you compile it, you will get error as

prog.cpp: In function 'int main()':
prog.cpp:8:11: error: lvalue required as left operand of assignment

what happens in the following code snippet?

#include <iostream>
using namespace std;

int& foo()
{
    return 2;
}

int main()
{
    foo() = 2;

    return 0;
}

The compiler will complain with rvalue as

prog.cpp: In function 'int& foo()':
prog.cpp:6:12: error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'

more examples

Let's assume we have an integer variable defined and assigned to:

int var;
var = 4;

An assignment expects an lvalue as its left operand, and var is an lvalue, because it is an object with an identifiable memory location. On the other hand, the following are invalid:

4 = var;       // ERROR!
(var + 1) = 4; // ERROR!

Neither the constant 4, nor the expression var + 1 are lvalues (which makes them rvalues). They're not lvalues because both are temporary results of expressions, which don't have an identifiable memory location (i.e. they can just reside in some temporary register for the duration of the computation). Therefore, assigning to them makes no semantic sense - there's nowhere to assign to.

So it should now be clear what the error message in the first code snippet means. foo returns a temporary value which is an rvalue. Attempting to assign to it is an error, so when seeing foo() = 2; the compiler complains that it expected to see an lvalue on the left-hand-side of the assignment statement.

Not all assignments to results of function calls are invalid, however. For example, C++ references make this possible:

int globalvar = 20;

int& foo()
{
    return globalvar;
}

int main()
{
    foo() = 10;
    return 0;
}

Here foo returns a reference, which is an lvalue, so it can be assigned to. Actually, the ability of C++ to return lvalues from functions is important for implementing some overloaded operators. One common example is overloading the brackets operator [] in classes that implement some kind of lookup access. std::map does this:

Modifiable lvalues

Initially when lvalues were defined for C, it literally meant "values suitable for left-hand-side of assignment". Later, however, when ISO C added the const keyword, this definition had to be refined. After all:

const int a = 10; // 'a' is an lvalue
a = 10;           // but it can't be assigned!

So a further refinement had to be added. Not all lvalues can be assigned to. Those that can are called modifiable lvalues.

Conversions between lvalues and rvalues

Generally speaking, language constructs operating on object values require rvalues as arguments. For example, the binary addition operator '+' takes two rvalues as arguments and returns an rvalue

int a = 1;     // a is an lvalue
int b = 2;     // b is an lvalue
int c = a + b; // + needs rvalues, so a and b are converted to rvalues
               // and an rvalue is returned

As we've seen earlier, a and b are both lvalues. Therefore, in the third line, they undergo an implicit lvalue-to-rvalue conversion. All lvalues that aren't arrays, functions or of incomplete types can be converted thus to rvalues

What about the other direction? Can rvalues be converted to lvalues? Of course not! This would violate the very nature of an lvalue according to its definition

This doesn't mean that lvalues can't be produced from rvalues by more explicit means. For example, the unary '*' (dereference) operator takes an rvalue argument but produces an lvalue as a result. Consider this valid code:

int arr[] = {1, 2};
int* p = &arr[0];
*(p + 1) = 10;   // OK: p + 1 is an rvalue, but *(p + 1) is an lvalue

Conversely, the unary address-of operator '&' takes an lvalue argument and produces an rvalue:

int var = 10;
int* bad_addr = &(var + 1); // ERROR: lvalue required as unary '&' operand
int* addr = &var;           // OK: var is an lvalue
&var = 40;                  // ERROR: lvalue required as left operand
                            // of assignment

The ampersand plays another role in C++. It allows to define reference types. These are called "lvalue references" πŸ‘Š.

  • Note that Non-const lvalue references cannot be assigned rvalues πŸ‘ŠπŸ‘Š, since that would require an invalid rvalue-to-lvalue conversion. For examples
int main()
{
 int x= 20;
 int & y = x +2; // ERROR (Non-const lvalue references cannot be assigned rvalues)
 int & y = x;  // OK
 std::cout<<y;
 std::cout<<x;
}
// error: invalid initialization of non-const reference of type 'int&' from an rvalue of
// type 'int'
int main()
{
 int x= 20;
 const int & y = x +2; // OK (Constant lvalue references can be assigned rvalues)
 std::cout<<y;
 std::cout<<x;
}
//2220
std::string& sref = std::string();  // ERROR: invalid initialization of
                                    // non-const reference of type
                                    // 'std::string&' from an rvalue of
                                    // type 'std::string'
  • Constant lvalue references can be assigned rvalues πŸ‘Š πŸ‘Š
    Since they're constant, the value can't be modified through the reference and hence there's no problem of modifying an rvalue.
    This makes possible the very common C++ idiom of accepting values by constant references into functions, which avoids unnecessary copying and construction of temporary objects.
int arr[] = {1, 2};
int* p = &arr[0];
*(p + 1) = 10;   // OK: p + 1 is an rvalue, but *(p + 1) is an lvalue

This note has been extracted from ref1

r-value referencing

Rvalue references solve at least two problems

  1. Implementing move semantics
  2. Perfect forwarding

Move semantics

Suppose X is a class that holds a pointer or handle to some resource, say, m_pResource.

X& X::operator=(cosnt X  & rhs)
{
  // [...]
  // Make a clone of what rhs.m_pResource refers to.
  // Destruct the resource that m_pResource refers to.
  // Attach the clone to m_pResource.
  // [...]
}

Now suppose X is used as follows

X foo();
X x;
// perhaps use x in various ways
x = foo();

The last line above

  • clones the resource from the temporary returned by foo,
  • destructs the resource held by x and replaces it with the clone,
  • destructs the temporary and thereby releases its resource.
    Rather obviously, it would be ok, and much more efficient, to swap resource pointers (handles) between x and the temporary, and then let the temporary's destructor destruct x's original resource
    In other words, in the special case where the right hand side of the assignment is an rvalue, we want the copy assignment operator to act like this:
// [...]
// swap m_pResource and rhs.m_pResource
// [...]

This is called move semantics. With C++11, this conditional behavior can be achieved via an overload.

X& X::operator=(<mystery type> rhs)
{
  // [...]
  // swap this->m_pResource and rhs.m_pResource
  // [...]  
}
  • Since we're defining an overload of the copy assignment operator, our "mystery type" must essentially be a reference: we certainly want the right hand side to be passed to us by reference.
  • Basically we want, when there is a choice between two overloads where one is an ordinary reference and the other is the mystery type, then rvalues must prefer the mystery type, while lvalues must prefer the ordinary reference πŸ‘ŠπŸ‘Š
  • If you now substitute "rvalue reference" for "mystery type" in the above, you're essentially looking at the definition of rvalue references.

Rvalue References

  • If X is any type, then X&& is called an rvalue reference to X.
  • For better distinction, the ordinary reference X& is now also called an lvalue reference.
  • The most important distinction between r-value and l-value referencing is, when it comes to function overload resolution, lvalues prefer old-style lvalue references, whereas rvalues prefer the new rvalue references
void foo(X& x); // lvalue reference overload
void foo(X&& x); // rvalue reference overload

X x;
X foobar();

foo(x); // argument is lvalue: calls foo(X&)
foo(foobar()); // argument is rvalue: calls foo(X&&)
  • So the gist is, Rvalue references allow a function to branch at compile time (via overload resolution) on the condition "Am I being called on an lvalue or an rvalue?"
  • It is true that you can overload any function in this manner, as shown above. But in the overwhelming majority of cases, this kind of overload should occur only for copy constructors and assignment operators, for the purpose of achieving move semantics
X& X::operator=(const X & rhs); // classical implementation
X& X::operator=(X&& rhs)
{
  // Move semantics: exchange content between this and rhs
  return *this;
}

Note, if you implement

void foo(X&);

but not

void foo(X&&);

then of course the behavior is unchanged: foo can be called on l-values, but not on r-values
But if you implement

void foo(X const &);

but not

void foo(X&&);

then again, the behavior is unchanged: foo can be called on l-values and r-values, but it is not possible to make it distinguish between l-values and r-values. To make possible distinguish between l-values and r-values, we need to implement following as well

void foo(X&&);

Important Note: if you implement void foo(X&&); but neither one of void foo(X&); and void foo(X const &); are implemented, then according to the final version of C++11, foo can be called on r-values, but trying to call it on an l-value will trigger a compile error.

This note has been extracted from ref2

⚠️ **GitHub.com Fallback** ⚠️