November 13, 2020

C++ lvalues and rvalues

By leonardo

When we are talking about lvalues and rvalues, it’s a good idea to back to the C language definition of these words. In C when we write some code like this:

int x = 1;

an lvalue is the left side of the expression and an rvalue is the right side of the expression. We can define it more technically by saying something like:

  • lvalue: is an expression that represents a space on the computer memory, in that space we can store an rvalue.
  • rvalue: a value that will be stored inside an lvalue. A literal value like 10, 20 or ‘a’.

Does it look simple, right? Not too fast! It probably would be very simple if we were talking about C, but it’s not true when talking about C++ because this one brought some concepts that made our life a little bit more complex. In C++ we need to handle classes, constant values and references, so this will modify our whole thinking about rvalues and lvalues.

Starting from the premise that the lvalue must be a space in the memory, it isn’t possible to read a value that isn’t in the memory. See the code below:

int* x = &1;

A pointer must point to a memory address, as you can see, we are trying to point the pointer x to a literal value, so your compiler will tell you the following but in other words: “the expression on the right side of the code should be an address of some part of your memory”.

Another pertinent example is about a function that returns a value, take a look at the code below:

int returnVal()
{
	return 99;
}

returnVal() = 10;

For obvious reasons we can’t assign a literal number to a function, for sure it’s not a common code snippet. Your compiler will tell you something like: “The expression on the left side of the assignment operator must be an lvalue”. He is right! We are returning an rvalue through the function and trying to assign a literal to it. But, what if it returns an lvalue?

To make this function return an lvalue we will need to return the value by reference, so, let’s modify the return type and the return itself.

static int Val = 99;

int& returnVal()
{
	return Val;
}

returnVal() = 10;

It will compile. Despite returnVal() = 10 isn’t a common expression, it will work because the returned value was an lvalue, which means it is allocated in some memory region, so we can assign it.

Maybe you are wondering about a simple expression like this:

int x = 99;
int y = 11;
int z = (x + y);

The expression (x+y) above (note that I put it inside parentheses just to clarify that it is a complete expression on the right side of the operand =. The expression (x+y) is an lvalue although it’s on the right side of the whole expression. Why?

It’s named lvalue-to-rvalue conversion that means an implicit conversion will be done when some lvalue is assigned to another lvalue. Well, it’s not too simple but you can find some further information about it in the C++ documentation at the following link: Value transformations C++

The cherry on top of C++

Now, after that introduction, we can talk about the const lvalue reference. See the code below:

int& x = 99;

As you know x is a reference to an lvalue, so it’s waiting for an lvalue to assign to. The sub expression 99 is a constant number, it’s not possible to assign it to a reference because it doesn’t has allocation on the memory, and there’s no storage reserved for this number.

The reason the code above doesn’t work is the same reason that the code below won’t compile.

void someOperation(int& val)
{
	return;
}

someOperation(99);

In the code we are passing a constant or literal number to a reference function argument. It means that the function was expecting an lvalue and we’ve passed an rvalue. On the other hand, the code below will work and we will be able to use both, literal values (rvalues) and lvalues. See it:

int a = 11;

void someOperation(const int& val)
{
	return;
}

someOperation(99);
someOperation(a);

The code above works because using const keyword we solved the problem of the modification of an rvalue, so now we can pass the data by reference even it’s just a number (rvalue). Why is this necessary? The answer you can find in the code below.

int a = 99;

void someOperation(int& val)	{}
void someOperation(int val)	{}

someOperation(11);
someOperation(a);

In the first case someOperation(11) the code works because we are passing a literal value, so there’s just one instance of the function someOperation that matches with the passed argument. But in the second case someOperation(a), the compiler isn’t able to handle the operation because the value passed is a and there are two functions that match with the passed argument.

This problem will block us to use the pass-by-reference technic to reduce the data copy to inside the function. So this way we will get a lower performance than if we were using pass-by-reference.

To handle this situation we need just to use the const operator inside the argument, this way we will secure the pass-by-reference technic and at the same time we can use both, lvalues and rvalues as the argument.

There’s a huge amount of other things about lvalues and rvalues that it’s not possible to explain in a single post, so I will try to explain C++ move semantics in another post.

All the best!