C++ - C++ Lvalue - C++ Lvalues and Rvalues - C++ Rvalue - Tech tutorial - Udacity Instructor Series

Inside Lvalues & Rvalues in C++.

In this post, we will look at two elementary categories of values in C++, namely Lvalues and Rvalues. Understanding both of them is important when it comes to passing data from one function to the other in an efficient manner in a program. The goal of this chapter is to explain the properties of both categories of values and to make clear their elementary importance to move semantics. 

Later, when we talk about Rvalues and Lvalue references, the basics from this post will help us better understand the essential mechanism behind Move semantics.

Value categories in C++.

Since C++11, we can classify any expression in our code into one of five value categories. Besides the data type, this is one of the basic properties of objects. Admittedly, the names for the expressions are sometimes confusing: there are Glvalues, Prvalues, Xvalues, Lvalues, and Rvalues.  Prior to C++11, there were essentially two categories, Lvalues and Rvalues, which we will focus on here for the most part. 

To understand the basic ideas of Move semantics, the other three categories are not essential. The figure below shows an overview of the available value categories. 

The interesting thing about the value categories is that based on them the compiler chooses a suitable operator or method – depending on which categories are contained in an expression. This is especially interesting when creating, copying or moving, i.e. both copy and move semantics. 

Let’s take a closer look at the two categories: Lvalues are objects that can be accessed by an address in memory. Such objects have an identifier that we can use in our code – so they are regular variables.

About Rvalues.

Rvalues, on the other hand, are objects in memory that are not accessible via an identifier. Such objects also exist only temporarily and are used, for example, to initialize variables or to evaluate operands. 

The letters L and R can be understood relatively well by the assignment operator: Lvalues are left of the equal sign and we assign a value to them, Rvalues are right of it.

However, the example is not correct in all cases, because there can also be lvalues that are on the right side. We will see this later in the code examples. It is still important that we can use the address operator to generate a rvalue from a lvalue, which we then assign to another lvalue. In the example you can see how this is meant:

We use the address operator to get the address of i. Although i is actually a regular variable and thus a lvalue, the associated address is an rvalue, i.e. there is no identifier through which we can access this value. But once the value has been made available in the short term with &i, we can initialize a pointer variable with it, which is again a lvalue. 

The idea behind this example is to demonstrate the interplay between Lvalues and Rvalues in the context of pointer variables – this will become very important later when we introduce the Rule of Five.

About Lvalues.

Now we come to another important concept, the Lvalue reference. Basically, it is an alternate identifier for the same object in memory. We can say that the reference for the stack memory is comparable to what the pointer is for the heap. When we make changes to the reference, it affects the lvalue that the reference points to. We can declare Lvalue references in code just as we did when passing parameters: We specify the data type and the identifier and write the address operator in front of it.

Then we can connect a lvalue to the reference via the assignment operator and we have already created an alias. Similar to heap pointers, it is also possible to define multiple alias identifiers associated with the same object.

We have already learned about Lvalue references when passing parameters and have seen that this eliminates the need to copy a value. Instead of copying, this simply passes a reference to the original. When we make changes to the reference, we also change the original. So far, when we have talked about references, we have always referred to Lvalue references. 

In the practical part, we will now look at some examples and use cases for Lvalue references.

Lvalues & Rvalues in practice.

We have learned so far that Lvalues are non-temporal objects in memory that we can access via an identifier. Somewhat simplifying, we can say that Rvalues are all other objects. In C++11 there are some other categories, but to understand the move semantics cleanly we don’t necessarily need them. We also learned about Lvalue references and saw that we can use them to create aliases for existing Lvalues and, for example, make a return from functions more efficient.

Experiment 1

In the first experiment, we want to look at some examples for lvalues and for rvalues.  In the theory section, we already saw that variables are lvalues. A variable is primarily an object in memory that has an identifier. The identifier can then be used to access the memory address from within the code. In contrast to a lvalue, an rvalue is basically nothing more than an object in memory without an identifier. 

Listing 13 gives some examples.  If we define a variable (→//1) and initialize it at the same time, we create, at least briefly, both a lvalue, i.e. the variable itself, and an rvalue. The latter is the value with which the variable is initialized, i.e. 0 in the example. The Rvalue 0 has no identifier and exists only for a short moment in memory.  The Lvalue i, however, is available from its definition until the end of the scope. So in this example, the assignment operator = copies a rvalue into a lvalue. However, we can also assign another lvalue to a lvalue (→//2).

int main()
{
// 1
    int i{0};      // i = Lvalue

// 2
int j{5};      // j = Lvalue
    j = i;         // j = Lvalue, i = Lvalue

// 3
int k = i + j; // k = Lvalue, i+j = Rvalue

// 4
    int *p = &i;   // p = Lvalue, &i = Rvalue
    // 5 = i;      // Error
    // &i = p;     // Error

return 0;
}

Listing 13: Using Lvalues and Rvalues

To do this, we first define a new variable j and initialize it with the rvalue 5. Then we use the assignment operator to copy the value j to the value i. Thus, both a rvalue and another value can be assigned to values. 

In the next example, we first use the addition operator + (→//3) to add two Lvalues and then the assignment operator = to assign the result to another Lvalue. The example is interesting because it seems that only lvalues are combined. However, this is actually not the case: although there are two lvalues on the right-hand side of =, the addition operator returns us one rvalue, namely the temporary result of adding i and j together. 

In the next example, we assign an address to a pointer variable (→//4). Here, the variable p is an Lvalue and the expression &i is again an Rvalue because the address operator returns the memory address of i as a temporary value without an identifier. However, if we try to assign a lvalue to an rvalue, this will not work. The instruction 5 = i will undoubtedly cause an error because it is trying to assign the lvalue i to the (temporary) rvalue 5. Likewise, we cannot assign the value of the pointer variable p to the address of i. So we can remember that a lvalue can be assigned other lvalues as well as rvalues. However, nothing can be assigned to an Rvalue, neither Lvalues nor Rvalues.

Experiment 2

In the second experiment, we want to use a Lvalue reference as an alias for a variable. To do this, we first define an integer variable l (→//1) in Listing 14 and initialize it with the value 0. Then we define a lvalue reference with the identifier l_ref and link it to the variable l using the assignment operator. 

If we then assign the value 5 to the alias l_ref and then output the value of the lvalue l, we can see in output 1 that the assignment to the alias changes the value of the linked variable.

int main()
{
// 1
int l{0};
    int &l_ref = l;
    l_ref = 5; 
    cout << "l = " << l << endl;

// 2
// int &ref = 5; // Error

return 0;
}
-----------------------
Output 1: 
l = 5

Output 2: 
Lvalue reference cannot bind to a temporary type (Rvalue)

Listing 14 : Lvalue reference as alias for a variable

Interestingly, the assignment in (→//2) does not work and the compiler also tells us the reason in output 2

Lvalue reference cannot bind to a temporary type (Rvalue).

So we can only assign other lvalues to a lvalue-reference and no rvalues. However, we will see shortly that there is still the Rvalue reference to which a Rvalue can be assigned. More details on this and the possible usefulness of such a construct will be discussed in detail in the next chapter.

Experiment 3

The third and final experiment is about lvalue references in functions. First, we will concentrate on the parameter part. We write a function named myFun1 (→//1a) in Listing 15a that has a lvalue reference as a parameter as well as no return value. In the function we multiply the parameter by 2 and since a Lvalue reference is an alias, this affects the original on the calling side. We now define this in main() as an integer variable m (→//1b), which is initialized with the value 10.

//2a
class MyClass
{
public:
    int _val;
    MyClass(int val) { _val = val; }
    ~MyClass() { cout << "~MyClass\n"; }
    MyClass returnCopy() { return *this; }
    MyClass &returnReference() { return *this; }
};

// 1a
void myFun1(int &param)
{
    param *= 2;
}

int main()
{
    // 1b
    int m{10};
    myFun1(m);
    cout << "m = " << m << endl;

    // 2b
    MyClass mc(5);
    cout << "mc._val (copy) = "
         << mc.returnCopy()._val << endl;

    // 2c
    cout << "mc._val (reference) = "
         << mc.returnReference()._val << endl;

    return 0;
}

Listing 15a : Lvalue references in functions

Next, we call the function myFun1() and pass the variable m as a parameter. To check the value of m, we output the variable to the console directly afterwards.

When we start the program, we see in Listing 15b at Output 1 as expected that the value of m has been doubled from 10 to 20. So with a lvalue reference as a parameter, we can pass results from functions back to the calling page and overwrite variables there. We have already seen in an earlier chapter that this technique can avoid costly copying when passing parameters, and additionally opens a channel for returning data from the function to the calling page.

Next, we will look at how a lvalue reference behaves as a return value and what benefit we can derive from it. As we saw in the last chapter when discussing the copy assignment operator, the goal of this technique is to avoid unnecessary copies of variables when returning data. To better understand the principle, we therefore write ourselves a class MyClass (→//2a) in Listing 15a.

Output 1: 
m = 20

Output 2: 
mc._val (copy) = 5
~MyClass
~MyClass

Output 3: 
mc._val (reference) = 5
~MyClass

Listing 15b: Output of example Lvalue references in functions

To keep the class simple, all data and methods are declared as public. The member variable _val of type Integer is initialized via the constructor of the class. In the destructor, only a string is written to the console so that we can easily read the number of created instances of MyClass from the number of outputs.

One of the core class functions for this example is returnCopy() , which returns a copy of the class instance to the caller via the dereferenced this pointer. Since this contains the address of the particular class instance, *this is a lvalue that is copied into the return type using return. 

The second core function returnReference() corresponds almost completely to the returnCopy() function, but has a lvalue reference as return type, which can be recognized by the address operator & in front of the function name. Now that the two functions are implemented, we create an instance of MyClass (→//2b) in main() and call the constructor with an integer, i.e. we set the _val paramater to the value 5.

Next, we use the returnCopy() function to create a copy of mc.

Its member _val is output to the console for control. Atoutput 2 in Listing 15b, we can see that the destructor was called twice – once for the instance mc and once for the temporary object created on return from returnCopy().

In (→//2c), the console output was changed to call returnReference() instead of returnCopy(). From output 3, we can see that the destructor of MyClass was called only once. So if we use a Lvalue reference as return type, then a temporary copy is avoided – so in the code example there is only one instance of MyClass at any time, namely mc. 

So we can use Return-By-Reference to achieve a more efficient return without copies. However, this does not apply in principle, because the returned object must still be valid after leaving the function. This applies, for example, to class instances as in the example, or even to global variables.  

Lvalues & Rvalues: In summary

In this chapter we got to know Lvalues, Lvalue references and Rvalues. Implicitly, we have already used all three types frequently, but without naming them in this form. A big advantage of Lvalue references is the possibility to create an alias to access variables. This can save memory and we open the possibility of a return channel to return data from functions without return. 

Since C++11 there is another type, the Rvalue reference. As we will see in the next chapter, this does for Rvalues what the Lvalue reference does for Lvalues. So we will now explore the question of what this somewhat exotic-looking construct can be used for. This much can be revealed: Rvalue references make move semantics possible in the first place. They are the crucial mechanism with which the idea of moving data can be put into practice.

Develop your C++ skills.

C++ is a compiled, high-performance programming language. Robots, automobiles, and embedded software all depend on C++ for speed of execution. Explore the C++ Developer Nanodegree to expand your software engineering skills into C++ development.

START LEARNING