More on C++ Value Categories
05 Jul 2020In the post related to C++ move semantics we briefly discussed about lvalues and rvalues. The complete story, however, is slightly more complex. We continue on the tales of Value Categories.
The Whole Picture
Before anything, it’s important to acknowledge the subject we are discussing about. The subject of this topic are all expressions. That includes but are not limited to variables (local, member, static, etc), temporarily expressions (results of operations, return values), functions.
There are total five value categories: rvalue, lvalue, glvalue, prvalue and xvalue:
However, only two definitions are important, and the rest can be inferred from the graph. These two are:
- rvalue: expressions that can be moved;
- glvalue: expressions that has identity;
We can get the definitions of the remaining three with $$ \mathbb{U} $$ as the set of all valid expressions:
- lvalue: $$ \mathbb{S}_l = \mathbb{U} - \mathbb{S}_r $$;
- prvalue: $$ \mathbb{S}{pr} = \mathbb{U} - \mathbb{S}{gl} $$;
- xvalue: $$ \mathbb{S}x = \mathbb{S}_r \bigcap \mathbb{S}{gl} $$.
Technical Definition
We won’t elaborate much further on the namings, and instead focus on the technical aspect of the two main definitions.
glvalue
A glvalue have identity. Technically speaking, having identity means the expression is not considered as a temporary, and its address can be taken. To verify whether an expression is a glvalue, we can attempt to compile
&expression; // compiler complains 'Cannot take adresss ...' if `expression` is not a glvalue
rvalue
An rvalue in moveable. To be moveable, an expression needs to be bind-able by an rvalue-reference. To verify whether an expression is a rvalue, we can attempt to compile
T&& var = expression; // rvalue-reference `var` can bind only to rvalues
Notable Examples
With the technical definitions of rvalue and glvalue, we can distinctly identify to which value category an expression falls. Any expression must fall into one and only one category from lvalue, xvalue and prvalue.
lvalue
An lvalue has identity and is not moveable. Typical examples of this include named variables, lvalue references.
/* named variable */
int i = 5;
//int&& r = i; // can't be moved 'Error: rvalue-reference cannot bind to lvalue'
int* p = &i; // has identity
/* a function that returns by reference */
int& funcRetByRef(int& a) { return a;}
//int&& r = funcRetByRef(i); // can't be moved
p = &funcRetByRef(i); // has identity
prvalue
An prvalue doesn’t have identity and is moveable. Typical examples are temporaries.
/* temporary expression `i+j` */
int i = 5, j = 6;
int&& r1 = i+j; // moveable
//int* p = &(i+j); // does not have identity 'Error: cannot take the address'
/* a function that returns by value */
int funcRetByVal() { int a = 0; return a;}
int&& r2 = funcRetByVal(); // moveable
//int* p = &funcRetByVal(); // does not have identity
xvalue
An xvalue has identity and is moveable.
The xvalues are the not so natural ones, and they have a strong touch of artificial in them.
Prime examples of xvalues are std::move(var1)
and std::forward(var2)
(when var2
is an rvalue), both of which rely on static_cast<T&&>
.
For verification, the trick to use address operator &
to determine whether an expression is glvalue no longer works on xvalues, as getting the address of xvalues is disallowed in C++:
int i = 5;
int&& r = std::move(i); // moveable
//int* p = &(std::move(i)); // has identity but 'Error: cannot take the address'
int* p = &i; // however, 'i' and 'std::move(i)' share the same address