Skip to content

Joseph Mansfield

The lvalue/rvalue metaphor

Every expression in C++ is either an lvalue or an rvalue. This distinction is what makes something like 5 = x; invalid, as the expression 5 is an rvalue expression and so cannot appear on the left of an assignment. This is in fact where the terms originate: only lvalues can appear on the left of an assignment, while rvalues must appear on the right [1] §3.10. However, this only really describes one particular case, so isn't useful for a general understanding of lvalues and rvalues.

For beginners, these value categories are a mild curiosity that appear only in error messages (for the above expression, GCC tells us "lvalue required as left operand of assignment"). However, since the introduction of move semantics and rvalue references in C++11, it has become more important than ever to understand them. One could try to learn every lvalue/rvalue rule in the standard, but there is a simple interpretation that can help you understand their purpose and reason about them. Lvalues and rvalues are just metaphors:

Lvalues and rvalues

Every useful C++ program revolves around the manipulation of objects, which are regions of memory created at runtime in which we store values. A simple int x;, for example, creates an object for storing integer values.

We also come across values that do not belong to any particular object. For example, the literal 5 represents the abstract value of 5, but is not stored in any object. Similarly, if we have two int objects, x and y, the expression x + y gives us a value representing the result of the addition — this value is also not stored in an object.

A simple interpretation of lvalues and rvalues is that lvalues represent objects and rvalues represent values. In the following code, x denotes an object, so it's an lvalue. x + 5 denotes a value, so it's an rvalue. The subexpression 5 is also an rvalue.

void foo(int x)
{
	bar(x); // the argument is an lvalue expression
	bar(x + 5); // the argument is an rvalue expression
}

I'm being careful here by using the word "represent". The truth is that rvalue expressions can denote objects too, but they still represent values. For example, some rvalue expressions result in the creation of a temporary object — such as a function call that returns by value [1] §5.2.2. Although an object does really exist here, the expression can still be thought of as just representing a value of that type. Consider this function:

std::string get_message() {
	return "Hello, World!";
}

Elsewhere in your code, the function call get_message() denotes the value of an std::string containing "Hello, World!", rather than a persistent object that you are going to manipulate.

Lvalue-to-rvalue conversion

Most operators in C++ expect rvalues (values) as their operands [1] §5. If we want to perform addition, for example, we just need two values to add together — we don't care if they belong to objects. A notable exception is the assignment operator, which requires an lvalue (object) for its left operand [1] §5.17. This is also logical — assignment needs an object in which to store something.

We can, however, also use lvalues where rvalues are expected — this is permitted by the implicit lvalue-to-rvalue conversion [1] §4.1. Once again, this makes sense — if we provide an object where a value is expected, we can just read the value of the object. That is, lvalue-to-rvalue conversion represents reading the value of an object..

Moving and forwarding

Both std::move and std::forward give you super powers: the ability to manipulate the value category of an expression.

A call to std::move is always an rvalue (value) expression. Because of this, std::move allows you to treat any expression as though it represents a value. What's the purpose of this? Well, objects are persistent regions of storage that we don't expect to change when doing non-destructive operations on them. However, if we know that we don't need the object any longer, we can often use destructive yet more efficient implementations. Values are inherently transient, so treating an object like a value allows us to perform these more efficient operations. For example, by treating objects as values, we can efficiently steal their resources when copying them (which we call moving, rather than copying). Look up move semantics to find out how to implement this for your classes.

In some cases, C++ will silently do this, treating your lvalues as rvalues [1] §12.8 (as though you had std::moved them). For example, when returning a local object from a function, the compiler knows that the object is no longer required and so can treat the returned expression as though it just represents a transient value:

widget foo()
{
	widget w;
	// ...
	return w; // the expression w is an lvalue, but is treated as an rvalue
}

std::forward relies on a neat little trick involving type deduction and reference collapsing. Consider the following example:

template<class T>
void wrapper(T&& x)
{
	foo(std::forward<T>(x));
}

When the argument passed to wrapper is an lvalue expression of type widget, x is deduced to be of type widget&. When it is an rvalue expression, x is of type widget&&. In both cases, the expression x will just be an lvalue. However, the std::forward function is cleverly designed so that std::forward(x) is an lvalue in the first case and an rvalue in the second case. Therefore, std::forward allows you to preserve whether an expression represented an object or a value.

Conclusion

This concludes a simple, intuitive intepretation for understanding lvalues and rvalues in which they are a metaphor for objects and values respectively. By thinking of lvalues and rvalues in this way, it becomes easier to reason about the behavior of your code and to make the most of modern C++.

[1] International Standard ISO/IEC 14882:2014(E) Programming Language C++