lvalues and rvalues are an important fundamental concept in C++, and are required to understand and use other C++ features such as Move Semantics. But what are lvalues and rvalues in C++, and how are they relevant?
What is an lvalue & rvalue in C++?
When it comes to identifying lvalues and rvalues, people sometimes mistakenly assume, that whatever is on the right hand side, is an rvalue, and whatever is on the left, is an lvalue. While this is actually true in many cases, this is not exactly the case. Let’s see why.
An lvalue is something that has memory associated with it. Something to which an rvalue can be assigned. Examples of an value include a simple variable, to which a value like “10” could be assigned.
int var = 10;
Conversely, an rvalue is something that has no memory associated with it, and cannot be assigned a value. Examples of rvalues include a simple constant like 10
. Now you obviously can’t do something like 10 = var
, right?
The reason why lvalues and rvalues simply don’t mean left and right values, is because we can do something like this:
int var1;
int var2;
var2 = var1;
In the above example, we have an lvalue on both the left and right side.
More Rvalues and Lvalues Examples
Let’s take a look at a few more cases to fully grasp the whole concept of lvalues and rvalues.
The below example shows two variables being added together, and assigned to a third variable. Do you see an rvalue here?
var3 = var1 + var2
There actually is an rvalue, and it is the following expression, var1 + var2
. While each of these two variables separately count as lvalues, summing them like this produces an rvalue.
If you want to do a quick check as to whether something is an rvalue or not, try to imagine assigning it a value.
Ask yourself, does the following code work?
var1 + var2 = var3
No, right? There is your answer.
Here is another interesting example.
int print(string &str) {
cout << str << endl;
}
int main() {
string str1 = "Hello";
string str2 = "World";
string str3 = str1 + str2;
print(str3); // Works
print(str1 + str2); // Doesn't work
}
The print()
function in the above example accepts a reference of an object, for which it requires the address. We tried to call the print()
function using str3
as a parameter and it works because the variable str3
has an address.
It won’t work if we try to pass str1 + str2
directly however, as it is an rvalue. This means our print() function is not compatible with rvalues. Luckily, there is a way to resolve this.
int print(const string &str) {
cout << str << endl;
}
int main() {
string str1 = "Hello";
string str2 = "World";
string str3 = str1 + str2;
print(str3); // Works
print(str1 + str2); // Works
}
Changing the print()
function to accept a const string&
, enables it to accept rvalues as well. Behind the scenes, the compiler does some magic, where it creates a temporary space for the rvalue in question. In short, our print()
function now works with both rvalues and lvalues, which is great!
rvalue references and temporary objects
An important use of rvalues, is that it can be used to refer to temporary objects. What if we wanted a way to ensure that we are dealing with temporary objects? Like when you use something like String&
, you know we are dealing with a reference to an object with a memory location, also known as an lvalue.
But as it turns out, with C++11, we have a way of dealing with rvalues as well using the && operator. Take a look at the below example.
string getString() {
return "Random String";
}
string& name = getString(); // INVALID
If you tried something like this, it’s obviously not gonna work right? Because getString() is an rvalue, and you can’t get a reference to it.
But, in C++11 this possible.
string getString() {
return "Random String";
}
string&& name = getString(); // VALID!
But how does this help us? Well this comes in handy when defining functions, becuase now we have a way of differentiating between an rvalue and an lvalue.
Let’s take a look at another example.
void Print(string& str) {
cout << "Lvalue Reference\n";
cout << str << "\n\n";
}
void Print(string&& str) {
cout << "Rvalue Reference\n";
cout << str << "\n\n";
}
int main() {
string str1 = "Hello";
string str2 = "World";
Print(str1);
Print(str1 + str2);
}
Here we have two different Print functions, where one accepts an rvalue reference, and one accepts an lvalue reference. We made two different calls, one with a lvalue, and one with an rvalue. Notice how the function calls are made by looking at the output.
Lvalue Reference
Hello
Rvalue Reference
HelloWorld
Still, you may ask, how is this useful? Well to understand that, you need to move on to Move Semantics. That’s where this concept actually comes in handy, and used for optimization and performance improvements.
Move Semantics
Now that you have understood lvalues and rvalues, the next step is to learn how Move Semantics work. We won’t cover that here, as it’s a separate topic, but here is the link to the Move Semantics Tutorial.
In the Move Semantics tutorial, you will learn how to utilize the concept of rvalues and lvalues, and combine them with other concepts to improve performance and memory costs of your code.
This marks the end of the What are Lvalues and Rvalues in C++? Tutorial. Any suggestions or contributions for CodersLegacy are more than welcome. Questions regarding the tutorial content can be asked in the comments section below.