Open In App

Understanding Lvalues, PRvalues and Xvalues in C/C++ with Examples

Improve
Improve
Improve
Like Article
Like
Save Article
Save
Share
Report issue
Report

LValue and RValue in C

Background

Quite a few of you who are about to read this article, might be looking for clarification of what used to be basic: rvalues were thingies that can pop up on the right of an assignment operator, and lvalues were thingies that belong on the left or right an assignment operator. After all, this is how k&R distinguished certain expressions from others: 

An object is a named region of storage; an lvalue is an expression referring to an object. An obvious example of an lvalue expression is an identifier with suitable type and storage class. There are operators that yield lvalues: for example, if E is an expression of pointer type, then *E is an lvalue expression referring to the object to which E points. The name “lvalue” comes from the assignment expression E1 = E2 in which the left operand E 1 must be an lvalue expression. The discussion of each operator specifies whether it expects lvalue operands and whether it yields an lvalue
 

Unfortunately, such simple approaches are now stubborn mementos of the dark ages. When we are courageous enough to consult the more recent specifications, we have paragraph §3.10 throwing the following taxonomy in our faces: 

             expression
          /       \
    glvalue       rvalue
       /      \      /      \
lvalue         xvalue        prvalue

Googling away for more human readable clarifications than the specification itself, the search results go into the difference between lvalue references and rvalue references, the fine nuances of move semantics, … All of these in fact advanced features that required this confusing hierarchy of fundamental concepts in the first place. 
Well, this text offers something quite different: it will try to make some sense out of all of this for people having their first look at these terms, without requiring mood-enhancing methods to get through it all… We can even offer the first advice we need to take to heart:

Forget about assignments and thingies to the left and the right of the assignment operator.

The biggest challenge in this tree of semantic labels, is the mysterious xvalue. We don’t have to understand xvalues, that is for the snobs. We can limit ourselves to understanding lvalues and prvalues. If you already understand xvalues, you can give your golden “elite C++ programmer” plaque a quick polish, and look for different articles on putting those xvalues to good use. For the rest of us, we can rephrase the current paragraph as the second advice:

Focus on understanding lvalues and prvalues in a variety of expressions.

Lvalue

We’re talking about the syntax and semantics of expressions, and assignments are cleverly buried into the BNF (Backus-Naur-Form) of such expressions. That is why the second advice recommends forgetting about assignments. Because the specification is still pretty clear on what an lvalue is! But rather than deciphering lengthy descriptions, let’s just provide some source code examples: 

// Designates an object
int lv1;         

// Reference, designates an object       
int &lv2        {lv1}

// Pointer, designates an object   
int *lv3;               

// Function returning a reference, designates an object
int &lv4() {            
  return lv1;
}

That’s it (more or less)! Well, we can figure out how classes are types, and class instances are also objects, and from there observe how references and pointers to instances and members are also objects; However, this is exactly the type of explanation that overwhelms us with detail to the point where it obscures the basics! At this point, we have a typical example for 4 different manifestations of lvalues. The spec doesn’t dictate any restrictions about it only belonging on the left-hand side or the right-hand side of an assignment operator! An lvalue is an expression that ultimately locates the object in memory.

Hence the much more appropriate description of lvalue as a “locator-value”.

At this point, we must admit that we snuck in an lvalue in an initialization expression: lv1 is not just an lvalue in the statement where it is declared! Even when lv1 is used to initialize the lv2 reference (a reference shall be initialized, always), lv1 is still an lvalue
The best way to illustrate the use of an lvalue, is to use it as a locator for result storage, as well as a locator of data input; Go ahead and look at them in action:

C++




// CPP program to illustrate the concept of lvalue
#include <iostream>
using namespace std;
  
// §3.10.1
// An lvalue designates a function or an object
// An lvalue is an expression whose
// address can be taken:
// essentially a locator value
int lv1{ 42 }; // Object
int& lv2{ lv1 }; // Reveference to Object
int* lv3{ &lv1 }; // Pointer to Object
  
int& lv4()
{
    // Function returning Lvalue Reference
    return lv1;
}
  
int main()
{
    // Examine the lvalue expressions
    cout << lv1 << "\tObject" << endl;
    cout << lv2 << "\tReference" << endl;
    cout << lv3 << "\tPointer (object)" << endl;
    cout << *lv3 << "\tPointer (value=locator)" << endl;
    cout << lv4() << "\tFunction provided reference" << endl;
  
    // Use the lvalue as the target
    // of an assignment expression
    lv1 = 10;
    cout << lv4() << "\tAssignment to object locator" << endl;
    lv2 = 20;
    cout << lv4() << "\tAssignment to reference locator" << endl;
    *lv3 = 30;
    cout << lv4() << "\tAssignment to pointer locator" << endl;
  
    // Use the lvalue on the right hand side
    // of an assignment expression
    // Note that according to the specification,
    // those lvalues will first
    // be converted to prvalues! But
    // in the expression below, they are
    // still lvalues...
    lv4() = lv1 + lv2 + *lv3;
    cout << lv1 << "\tAssignment to reference locator (from function)\n"
                   "\t\tresult obtained from lvalues to the right of\n"
                   "\t\tassignment operator"
         << endl;
  
    return 0;
}


Output: 

42    Object
42    Reference
0x602070    Pointer (object)
42    Pointer (value=locator)
42    Function provided reference
10    Assignment to object locator
20    Assignment to reference locator
30    Assignment to pointer locator
90    Assignment to reference locator (from function)
        result obtained from lvalues to the right of
        assignment operator

 

Prvalue

We are skipping the more complicated rvalue for now. In the aforementioned dark ages, they were trivial. Now they include the mysterious sounding xvalues! We want to ignore those xvalues , which is exactly what the definition of a prvalue lets us do: 

A prvalue is an rvalue that is not an xvalue. 
Or with a bit less obfuscation:

A prvalue represents a direct value.

This is most obvious in an initializer:  

int prv1                {42};   // Value

However, another option is to use an lvalue to initialize: 

constexpr int lv1       {42};
int prv2                {lv1};  // Lvalue

What is happening here! This was supposed to be simple, how can an lvalue be a prvalue??? In the specification, §3.10.2 has a sentence that comes to the rescue:

Whenever a glvalue appears in a context where a prvalue is expected, the glvalue is converted to a prvalue.

Let’s ignore the fact that a glvalue is nothing else than an lvalue or an xvalue. We have already banned xvalues from this explanation. Hence: how do we get a value (prvalue) from the lvalue rv2? By converting (evaluating) it! 

We can make it even more interesting:  

constexpr int f1(int x} {
  return 6*x;
}
int prv3  {f1(7)};  // Function return value 

We now have a function f1(), that returns a value. The specification indeed provides for situations where a temporary variable (lvalue) gets introduced, which then will get converted to a prvalue where needed. Just pretend that this is happening: 

int prv3 {t}; // Temporary variable t created by compiler
                   // . not declared by user),
                   // - initialized to value returned 
                   // by f1(7)

A similar interpretation is in place for more complex expressions: 

int prv4 {(lv1+f1(7))/2};// Expression: temporary variable
                                    //  gets value of (lv1+f1(7))/2

Careful now! The rvalues are NOT the objects, nor the functions. The rvalues are what is ultimately used:  

  • The value of a literal (not related to any object).
  • The value returned by a function (not related to any object, unless we count the temporary object used for the return value).
  • The value of a temporary object is required to hold the result of evaluating an expression.

For the people who learn by executing a compiler:  

C++




// CPP program to illustrate glvalue
#include <iostream>
using namespace std;
  
// §3.10.1
// An rvalue is an xvalue, a temporary object (§12.2),
// or a value not associated with an object
// A prvalue is an rvalue that is NOT an xvalue
  
// When a glvalue appears in a context
// where a prvalue is expected,
// the glvalue is converted to a prvalue
int prv1{ 42 }; // Value
  
constexpr int lv1{ 42 };
int prv2{ lv1 }; // Expression (lvalue)
  
constexpr int f1(int x)
{
    return 6 * x;
}
int prv3{ f1(7) }; // Expression (function return value)
  
int prv4{ (lv1 + f1(7)) / 2 }; // Expression (temporary object)
  
int main()
{
    // Print out the prvalues used
    // in the initializations
    cout << prv1 << " Value" << endl;
    cout << prv2 << " Expression: lvalue" << endl;
    cout << prv3 << " Expression: function return value" << endl;
    cout << prv4 << " Expression: temporary object" << endl;
  
    return 0;
}


Output: 

42 Value
42 Expression: lvalue
42 Expression: function return value
42 Expression: temporary object

 

Xvalue

Wait: we were not going to talk about xvalues?! Well, at this point, we have learned that lvalues and prvalues are really not that hard after all. Pretty much what any reasonable person would expect. We don’t want to be disappointed by reading all this text, only to confirm that lvalues involve a locatable object, and prvalues refer to some actual value. Hence this surprise: we might as well cover xvalues as well, then we’re done and understand all of them! 
We need to embark on a bit of a story to get to the point, though…

References 
The story starts in §8.5.3 in the specification; We need to understand that the C++ now distinguishes between two different references

int&  // lvalue reference
int&&  // rvalue reference

Their functionality is semantically exactly the same. Yet they are different types! That means that the following overloaded functions are different as well: 

int f(int&);
int f(int&&);

This would be silly, if it wasn’t for this one sentence in the specification that no normal human beings ever reach, well into §8.5.3: 

A reference to type “cv1 T1” is initialized by an expression of type “cv2 T2” as follows: 
… 
If the reference is an rvalue reference, the initializer expression shall not be an lvalue.

Looking at a simple attempt to bind references to an lvalue:  

int lv1         {42};
int& lvr        {lv1};    // Allowed
int&& rvr1      {lv1};   // Illegal
int&& rvr2      {static_cast<int&&>(lv1)};// Allowed

This particular behavior can now be exploited for advanced features. If you want to play with it a bit more, here is a jump start: 
(manipulate line 33 to enable the illegal statements). 

C++




#include <iostream>
using namespace std;
  
// §8.3.2
// References are either form of:
// T& D         lvalue reference
// T&& D        rvalue reference
// They are distinct types (differentiating overloaded functions)
  
// §8.5.3
// The initializer of an rvalue reference shall not be an lvalue
  
// lvalue references
const int& lvr1{ 42 }; // value
  
int lv1{ 0 };
int& lvr2{ lv1 }; // lvalue (non-const)
  
constexpr int lv2{ 42 };
const int& lvr3{ lv2 }; // lvalue (const)
  
constexpr int f1(int x)
{
    return 6 * x;
}
const int& lvr4{ f1(7) }; // Function return value
  
const int& lvr5{ (lv1 + f1(7)) / 2 }; // expression
  
// rvalue references
const int&& rvr1{ 42 }; // value
  
// Enable next two statements to reveal compiler error
#if 0
int&& rvr2       {lv1}; // lvalue (non-const)
const int&& rvr3  {lv2}; // lvalue (const)
#else
int&& rvr2{ static_cast<int&&>(lv1) }; // rvalue (non-const)
const int&& rvr3{ static_cast<const int&&>(lv2) }; // rvalue (const)
#endif
const int&& rvr4{ f1(7) }; // Function return value
const int&& rvr5{ (lv1 + f1(7)) / 2 }; // expression
  
int main()
{
    lv1 = 42;
    // Print out the references
    cout << lvr1 << " Value" << endl;
    cout << lvr2 << " lvalue (non-const)" << endl;
    cout << lvr3 << " lvalue (const)" << endl;
    cout << lvr4 << " Function return value" << endl;
    cout << lvr5 << " Expression (temporary object)" << endl;
  
    cout << rvr1 << " Value" << endl;
    cout << rvr2 << " rvalue (const)" << endl;
    cout << rvr3 << " rvalue (non-const)" << endl;
    cout << rvr4 << " Function return value" << endl;
    cout << rvr5 << " Expression (temporary object)" << endl;
  
    return 0;
}


Output: 

42 Value
42 lvalue (non-const)
42 lvalue (const)
42 Function return value
21 Expression (temporary object)
42 Value
42 rvalue (const)
42 rvalue (non-const)
42 Function return value
21 Expression (temporary object)

 

Move Semantics

The next part of the story needs to be translated from §12.8 in the specification. Rather than copying objects, it might be faster (especially for large objects) if object resources can be “moved”. This is relevant in two different situations: 

  1. Initialization (including argument passing and value return).
  2. Assignment.

These situations rely on special member functions to get the job done:  

struct S {
  S(T t) : _t(t) {}  // Constructor
  S(const S &s); // Copy Constructor
  S& operator=(const S &s); // Copy Assignment Operator
  T* _t;
};

T t1;
S s1    {t1};    // Constructor with initialization
S s2    {s1};    // Constructor with copy
S s3;        // Constructor with defaults
s3 = s2;    // Copy assignment operator

How innocent does that pointer to T look in the declaration of struct S! However, for large, complex types T, the management for the content of the member _t can involve deep copies and really smack down the performance. Every time an instance of struct S goes through the parameters of a function, some expressions, and then potentially a return from a function: we’re spending more time copying data, than effectively working with it! 
We can define some alternative special functions to deal with this. These functions can be written in such a way that rather than copying information, we just steal it from other objects. Only we don’t call it stealing, it involves a much more legal terminology: moving it. The functions take advantage of the different types of references: 

S(const S &&s); // Move Constructor
S& operator=( S &&s); // Move Assignment Operator

Note that we’re keeping the original constructor and operator for when the actual parameter is an lvalue
However, if we only could force the actual parameter to be an rvalue, then we can execute this new constructor or assignment operator! There actually are a few ways of turning the lvalue into an rvalue; A trivial way is to static_cast the lvalue to the appropriate type: 

S s4 {static_cast<S&&>(s3)); // Calls move constructor
s2 = static_cast<S&&>(s4); // Calls move assignment operator 

The same can be achieved in a bit more comprehensive way, by indicating that the parameter “can be used to move data”:  

S s4 {std::move(s3)); // Calls move constructor
S2 = std::move(s4); // Calls move assignment operator 

The best insight is always to see it in action:  

C++




#include <iostream>
using namespace std;
  
// §12
// Special member functions
//  . §12.1     Constructor
//  . §12.8     Copy/Move
//    - §12/1   Copy/Move Constructor
//    - §13.5.3 Copy/Move Assignment Operator
struct T {
    int _v1;
    int _v2;
    int _v3;
  
    friend std::ostream& operator<<(std::ostream& os, const T& p)
    {
        return os << "[ " << p._v1 << " | " << p._v2 << " | " << p._v3 << " ]";
    }
};
  
struct S {
    S() // Constructor
    {
        cout << "Constructing instance of S" << endl;
        _t = new T{ 1, 2, 3 };
    }
    S(T& t) // Constructor
    {
        cout << "Initializing instance of S" << endl;
        _t = new T{ t };
    }
  
    S(const S& that) // Copy Constructor
    {
        cout << "Copying instance of S" << endl;
        _t = new T;
        *_t = *(that._t); // Deep copy
    }
    S& operator=(const S& that) // Copy Assignment Operator
    {
        cout << "Assigning instance of S" << endl;
        *_t = *(that._t); // Deep copy
        return *this;
    }
  
    S(S&& that) // Move Constructor
    {
        cout << "Moving instance of S" << endl;
        _t = that._t; // Move resources
        that._t = nullptr; // Reset source (protect)
    }
    S& operator=(S&& that) // Move Assignment Operator
    {
        cout << "Move-assigning instance of S" << endl;
        _t = that._t; // Move resources
        that._t = nullptr; // Reset source (protect)
        return *this;
    }
  
    T* _t;
};
  
int main()
{
    T t1{ 41, 42, 43 };
    cout << t1 << " Initializer" << endl;
    S s1{ t1 };
    cout << s1._t << " : " << *(s1._t) << " Initialized" << endl;
  
    S s2{ s1 };
    cout << s2._t << " : " << *(s2._t) << " Copy Constructed" << endl;
  
    S s3;
    cout << s3._t << " : " << *(s3._t) << " Default Constructed" << endl;
    s3 = s2;
    cout << s3._t << " : " << *(s3._t) << " Copy Assigned" << endl;
  
    S s4{ static_cast<S&&>(s3) };
    cout << s4._t << " : " << *(s4._t) << " Move Constructed" << endl;
  
    s2 = std::move(s4);
    cout << s2._t << " : " << *(s2._t) << " Move Assigned" << endl;
  
    return 0;
}


Output: 

[ 41 | 42 | 43 ] Initializer
Initializing instance of S
0x1d13c30 : [ 41 | 42 | 43 ] Initialized
Copying instance of S
0x1d13c50 : [ 41 | 42 | 43 ] Copy Constructed
Constructing instance of S
0x1d13c70 : [ 1 | 2 | 3 ] Default Constructed
Assigning instance of S
0x1d13c70 : [ 41 | 42 | 43 ] Copy Assigned
Moving instance of S
0x1d13c70 : [ 41 | 42 | 43 ] Move Constructed
Move-assigning instance of S
0x1d13c70 : [ 41 | 42 | 43 ] Move Assigned

 

Xvalues

We have reached the end of our story: 

xvalues are also known as eXpiring values.

Lets have a look at the move semantics of the example above:  

  S(S &&that) // Move Constructor
  {
    cout << "Moving instance of S" << endl;
    _t = that._t;     // Move resources
    that._t = nullptr;  // Reset source (protect)
  }
  S& operator=(S &&that)  // Move Assignment Operator
  {
    cout << "Move-assigning instance of S" << endl;
    _t = that._t;      // Move resources
    that._t = nullptr;  // Reset source (protect)
    return *this;
  }

We have achieved the goal of performance by moving the resources from the parameter object, into the current object. But note that we’re also invalidating the current object right after that. This is because we don’t want to accidentally manipulate the actual parameter object: any change there would ripple through to our current object, and that is not quite the encapsulation we’re after with object-oriented programming. 
The specification gives a few possibilities for an expression to be an xvalue, but let’s just remember this one: 

  • A cast to an rvalue reference to an object…

Summary 

Lvalues (Locator values) Designates an object, a location in memory
Prvalues (Pure rvalues) Represents an actual value
Xvalues (eXpiring values An object towards the end of its’ lifetime (typically used in move semantics)

 



Last Updated : 29 Sep, 2022
Like Article
Save Article
Previous
Next
Share your thoughts in the comments
Similar Reads