Before we talk about the next compound type (references), we’re going to take a little detour.
In C++, every expression has two properties: type
and value category
.
Since expression always resolve to a single value, the type of the resultant value is the type of the expression.
You’ve already seen examples of the type
property of expressions:
1 2 3 4 5 6 7 8 |
#include <iostream> int main() { auto v1 { 12 / 4 }; // int / int => int auto v2 { 12.0 / 4 }; // double / int => double return 0; } |
Both variable v1
and v2
have initializers that are simple expressions.
For v1
, the compiler will determine that a division with two int
operands will produce an int
result, so that is the type of this expression. Via type inference, that int
type will then be used as the type of v1
.
For v2
, the compiler will determine that a division with a double
and an int
operand will produce a double
result (remember that arithmetic operators must have operands of matching types, so the int
operand gets promoted to a double
, and a floating point division is performed).
Compound expressions (those with multiple operators) work the same way -- the compiler uses the precedence and associativity rules to determine in what order the expression will evaluate. The type of the result is the type of the expression.
Note that the type of an expression can always be determined at compile time (if it couldn’t, type deduction wouldn’t work) -- however, the value of the expression may be determined at either compile time (if the expression is constexpr) or runtime (if the expression is not constexpr).
Value categories
Now consider the following program:
int main()
{
int x{};
x = 5; // valid
5 = x; // invalid
return 0;
}
One of these assignment statements is valid (assign the value 5 to variable x) and one is not (what would it mean to assign the value of x to the literal value 5?). So how does the compiler know which values can legally appear on which side of an assignment statement?
The second property of expressions is called the value category
. An expression’s value category helps the compiler determine how to create, copy, and move objects during the evaluation of the expression.
Since assignments are a type of copy operation, the value category is used to determine whether a given assignment is legal.
As of C++11, C++ has 5 different kinds of value categories. In this lesson, we’ll explore a simplified view of two most common ones (l-values and r-values). The other three are used for move semantics, which we’ll cover in a future chapter.
// http://en.cppreference.com/w/cpp/language/value_category
// https://stackoverflow.com/questions/3601602/what-are-rvalues-lvalues-xvalues-glvalues-and-prvalues
l-values
An l-value (short for “locator value”, or “left value”, and sometimes written as lvalue
) is an expression that refers to an object with an identifiable memory address. The name l-value came about historically because l-values were expressions that could legally appear on the left-hand side of an assignment expression. Variables are an example of an l-value, as they have a discrete memory address.
1 2 3 4 5 6 7 |
int main() { int x{}; x = 2; // success: x is an l-value return 0; } |
Since the introduction of constants into the language, l-values now come in two subtypes: a modifiable l-value is an l-value whose value can be modified. A non-modifiable l-value is an l-value whose value can’t be modified (typically because the l-value is const or constexpr). Clearly non-modifiable l-values are not legal targets for being assigned to, so it’s now more accurate to say that the left-hand side of an assignment must be a modifiable l-value.
We can now answer the question of why 5 = x;
is not valid -- it’s because 5
is not an l-value. So what is 5
?
r-values
The opposite of l-values are r-values (pronounced arr-values). R-values are defined by negation: an r-value is any expression that is not an l-value. Common r-values include literals, temporary and anonymous objects (which we’ll discuss later in this chapter), and function calls that return values by copy (which are a type of temporary object). R-value expressions are typically evaluated for their values.
In many cases, l-values will implicitly convert to r-values, which is why an expression like this works fine:
1 2 3 4 5 6 7 8 9 |
int main() { int x { 2 }; int y { 3 }; x = y; // y (which is an l-value) will be treated as an r-value in this context return 0; } |
Now consider this snippet:
1 2 3 4 5 6 7 8 |
int main() { int x { 2 }; x = x + 1; return 0; } |
In this statement, the variable x
is being used in two different contexts. On the left side of the assignment operator, x
is being used as an l-value (variable with an address). On the right side of the assignment operator, x
is being used as an r-value, and will be evaluated to produce a value (in this case, 2
). When C++ evaluates the above statement, it evaluates as:
1 |
x = 2 + 1; |
Which makes it obvious that C++ will assign the value 3
back into variable x
.
Note that function return values by copy are an r-value:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <iostream> enum class Pet { dog, cat, pig, rock }; Pet getDog() { return Pet::dog; } int main() { Pet p{ Pet::dog }; // p is an l-value p = Pet::cat; // okay to assign to l-value -- woo...meow getDog() = Pet::cat; // error: result of getDog() is an r-value return 0; } |
In the above example, getDog() return an r-value, so it can not be assigned to.
Temporary objects
// move to chapter on temporary and anonymous objects
In certain cases, the compiler will create temporary objects for us. For example:
1 2 3 4 5 6 7 8 |
int main() { int x { 2 }; x = x + 1; return 0; } |
When the compiler evaluates x+1
, the result 3
must be stored somewhere before it is assigned back to x
. A temporary object is constructed to store the value until it can be assigned.
The Pet example above is another example -- the return value of getDog() returns a temporary object of type Pet. We must use this return value immediately or it will be discarded.
These expressions that create temporary objects are r-values. For the most part, you don’t need to worry about temporary objects, as they’re created, used, and destroyed implicitly. Later in this chapter, we’ll talk about how you can intentionally create your own temporary objects.
![]() |
![]() |
![]() |
Leave a Reply