In this third post, we will look in detail at Rvalue references – alias identifiers for temporary values. Such a concept may sound relatively trivial at first glance, but in fact, it is the mechanism that makes Move semantics and the ideas behind it possible in the first place. Let’s take a look at why this is so.
What are RValue references?
In the second post, we already learned about Rvalues as temporary values that are used to initialize variables, for example. Rvalues are not accessible to us via their own address, although they are an object in memory just like Lvalues. Here in the example, the variable k is an Lvalue and the 5 on the right side of the assignment operator is an Rvalue.
C++11 introduced the Rvalue reference for the first time, which is a tool that allows us to get permanent access to temporary objects in memory.
Rvalue references work in principle similarly to Lvalue references: We declare them by writing the data type of the rvalue followed by && and an identifier. Then, as shown in the example, we can use the assignment operator to connect the rvalue 42 to the reference j:
Once the allocation is complete, we have read and write access to the memory location where the number 42 is stored. So basically, we have turned the temporary Rvalue 42 into a permanent Lvalue j that exists as long as the Rvalue reference is valid.
One of the main ideas behind Rvalue references is to make assignments more efficient by saving copy operations. It’s best to look at how a normal assignment works without Rvalue references.
In the example, the sum of two variables k and l is assigned to a variable m. The variables are all lvalues, but the sum k + l is an rvalue because the addition operator returns the result only temporarily and without an identifier.
The instruction in the example can be divided into four individual steps:
- In the first step, the lvalue m is created in memory.
- The sum k + l is calculated and the result is also stored in memory, but as an Rvalue.
- This temporary value is then copied into the address of m. For a short moment the value exists twice and therefore consumes twice the memory. 4.
- Afterward the temporary value is released again and can be deleted by the system.
A simple value assignment thus causes two objects in memory, a copy operation and a delete operation. Now let’s look at the same thing again with an Rvalue reference instead of a Lvalue.
One of the great advantages of Rvalue references is that they save copy operations when assigning values. The idea is to use the result of e.g. an addition operation directly without having to copy the value. The example shows the principle:
First, an Rvalue reference n is created in memory. The addition operator next computes the sum of the two lvalues k and l, and then the address of the temporary result is assigned to n without copying the actual value. Compared to the conventional value assignment Rvalue to Lvalue, we thus save memory, a copy operation, and a delete operation. In general, the amount of savings is proportional to the size of the data type.
In code, Rvalue references can then be used like regular identifiers and thus Lvalues to access the formerly temporary objects in memory.
We have already seen, among other things, in copy semantics that Lvalue references can be used very effectively as function parameters. We thus save copy operations when passing arguments by creating a local alias instead. The alias can be used to directly access the argument in the original from within the function.
The same works with rvalue references. If we pass an Rvalue as an argument instead of an Lvalue, then it will no longer be copied, but an alias will be created as well. We can access the memory via the alias and read or write.
On the calling side, the call would then look like this, for example:
Here we pass the Rvalue 42 to the passValue() function. The main difference to Lvalue references is that on the function call side, i.e. in main(), the Rvalue is no longer needed. It is only available inside the function we passed it to. So we can use rvalue references to make passing rvalues to functions more efficient, because just as with lvalue references, we don’t need to copy the arguments.
So, like Lvalue references, Rvalue references offer us a way to increase efficiency. Now let’s look at why, in addition, they also form the basis of Move semantics: The basic idea of move semantics is to avoid expensive deep copy operations and use cheaper move operations instead.
Just now we saw that Lvalue references and Rvalue references are almost the same thing: They provide us with an alias to a memory value and save a copy operation. In the practical part we will see in a moment that we cannot assign a rvalue to a lvalue reference and a lvalue to a rvalue reference. If we write a function that should be able to do both, then we have to overload it accordingly (i.e. write two variants of it) with both a lvalue reference and a rvalue reference as parameters.
The compiler can tell the difference and calls the correct variant depending on the argument passed. This ability to distinguish between lvalues and rvalues is the basic principle by which move semantics works: We have the possibility to detect temporary objects, i.e. Rvalues, and thus treat them differently within a function than Lvalues.
In the practical part, we will now look at some examples on Rvalue references before implementing the actual move operations in the next post.
RValue references in practice.
So far we have learned about the Rvalue reference as a counterpart to the Lvalue reference, i.e. as an alias identifier for (temporary) objects. However, before we get into the meaning of the Rvalue reference for move semantics, the point here is to get to know the rules of the game when using this construct in practice.
In the first experiment, we want to look at the difference between Lvalue references and Rvalue references. To do this, we first create a Lvalue in Listing 16, namely a variable i of type integer (→//1).
Then we create a lvalue reference named lv_ref and link it to i. It is important to note that the reference must have the same data type as the variable. As can be seen from the commented out line, no rvalues can be assigned to a lvalue reference. The compiler generates the following error message for this case: lvalue reference to type ‘int’ cannot bind to a temporary of type ‘int’ .
Next, we want to create a rvalue reference rvref (→//2). To do this, we prefix the identifier with the double Kaufmans And and link the reference to the temporary value 10. Due to the link, this value remains valid beyond the end of the statement and is valid until the scope of rvref is left again. Thus, we have used an rvalue reference to change the lifetime of a temporary object.
If we now test changing the 10 to the value i we get again an error message: rvalue reference to type ‘int’ cannot bind to lvalue of type ‘int.’
So we can only assign a temporary rvalue to a rvalue reference and no lvalue. However, if we subsequently assign the value 20 to rvref and then output the value of the rvalue reference rvref to the console, then output 2 shows that the change has directly affected the original object in memory. So we can use rvalue references to make temporary values permanently available and change them.
In the second experiment, we want to look at the compiler’s ability to distinguish Lvalues from Rvalues and call the corresponding function variant depending on the type of argument passed.
void passValue(int &¶m)
cout << "Rvalue\n";
void passValue(int ¶m)
cout << "Lvalue\n";
int &lv_ref = i;
// int &lv_ref = 5; // Error
int &&rv_ref = 10;
// int &&rv_ref = i; // Error
rv_ref = 20;
cout << "rv_ref = " << rv_ref << endl;
Listing 16: Experiments with rvalue references.
rv_ref = 20
Outputs for Listing 16
Listing 16 therefore contains two variants of the passValue() function for both Lvalues with & and Rvalues with &&. Within the functions, a corresponding string is output to the console. In main() we now call the function name first with the lvalue i and then with an rvalue 5 as argument (→//3). From output 3 we can see that the compiler is able to choose the correct function according to the argument. In the next post, we’ll look at this again in detail in the context of the move constructor and move assignment operator.
Finally, let’s discuss one more example. We have already created a rvalue reference with rvref and used it for assignments. The question now arises which variant of passValue() is called when we pass rvref, i.e. an Rvalue reference, as an argument (→//4).
In output 4 we can see that, interestingly, rv_ref was interpreted by the compiler not as an Rvalue, but as an Lvalue. This shows a very important connection: rvalue references are basically lvalues: if we attach an identifier to a temporary value in memory and thus make it available in the entire scope, then the rvalue reference behaves no differently than a lvalue. And that’s why it only makes sense here if the compiler calls the Lvalue variant of the function.
So Rvalue references allow to create an alias for temporary values. The compiler can tell when a function is called whether an lvalue or an rvalue was passed. In the next post we will see how this is made possible by the move semantics.
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.