Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Understanding Lvalues and Rvalues
Understand value categories (lvalues and rvalues) for efficient resource management in modern C++.
Value Categories: Lvalues and Rvalues
Before exploring our first compound type, we need to understand an important concept: value categories. This knowledge is essential for working with references and other advanced features.
Earlier, we learned that expressions combine literals, variables, operators, and function calls to produce a value:
#include <iostream>
int main()
{
std::cout << 10 + 15 << '\n'; // Expression 10 + 15 produces value 25
return 0;
}
In this program, the expression 10 + 15 evaluates to 25, which is printed.
Expressions can also produce side effects that persist after evaluation:
#include <iostream>
int main()
{
int count{0};
++count; // Side effect: count is modified
std::cout << count << '\n'; // Prints 1
return 0;
}
The expression ++count increments count, and this change persists after the expression completes.
Additionally, expressions can evaluate to objects or functions, which we'll examine shortly.
Expression Properties
All C++ expressions have two properties: a type and a value category.
Expression Type
An expression's type matches the type of the value, object, or function it produces:
int main()
{
auto a{20 / 4}; // int / int => int
auto b{20.0 / 4}; // double / int => double
return 0;
}
For a, dividing two int values produces an int result, so the expression type is int. Through type inference, a becomes int.
For b, dividing a double by an int produces a double (after the int is converted to double). So b becomes double.
The compiler uses expression types for validation:
#include <iostream>
void display(int value)
{
std::cout << value << '\n';
}
int main()
{
display("text"); // Error: display expects int, not string
return 0;
}
The display(int) function expects an int, but receives a string literal. Since no conversion exists, compilation fails.
Expression types must be determinable at compile time for type checking. However, expression values may be determined at compile time (constexpr) or runtime (non-constexpr).
Value Category
Consider this program:
int main()
{
int score{};
score = 100; // Valid: assign 100 to score
100 = score; // Error: cannot assign to literal 100
return 0;
}
One assignment works, the other doesn't. Why? The answer lies in value categories.
The value category indicates whether an expression resolves to a value, function, or object. Prior to C++11, only two categories existed: lvalue and rvalue.
C++11 added three more categories (glvalue, prvalue, xvalue) to support move semantics, which we'll cover in a future chapter. For now, we'll focus on the traditional lvalue and rvalue categories.
Lvalues and Rvalues
An lvalue (pronounced "ell-value," short for "left value" or "locator value") is an expression evaluating to an identifiable object or function.
"Identity" means an entity can be distinguished from similar entities, typically by comparing addresses. Entities with identity can be accessed via identifiers, references, or pointers, and have lifetimes exceeding a single expression.
int main()
{
int balance{1000};
int withdrawal{balance}; // balance is an lvalue
return 0;
}
Here, balance is an lvalue because it evaluates to the variable balance, which has an identifier.
Lvalues come in two flavors:
- Modifiable lvalue: Can be modified
- Non-modifiable lvalue: Cannot be modified (const or constexpr)
int main()
{
int mutable_value{};
const double immutable_value{};
int copy1{mutable_value}; // mutable_value is a modifiable lvalue
const double copy2{immutable_value}; // immutable_value is non-modifiable
return 0;
}
An rvalue (pronounced "arr-value," short for "right value") is any expression that isn't an lvalue. Rvalues evaluate to values without identity, existing only within their expression's scope. Literals (except C-style strings) and functions returning by value are common rvalues.
int computeSum()
{
return 42;
}
int main()
{
int num{10}; // 10 is an rvalue
const double pi{3.14159}; // 3.14159 is an rvalue
int result{num}; // num is a modifiable lvalue
const double circle{pi}; // pi is a non-modifiable lvalue
int answer{computeSum()}; // computeSum() is an rvalue (returns by value)
int total{num + 5}; // num + 5 is an rvalue
int rounded{static_cast<int>(pi)}; // static_cast result is an rvalue
return 0;
}
Why are computeSum(), num + 5, and static_cast<int>(pi) rvalues? They produce temporary values without identifiable objects.
Core Understanding
Lvalues evaluate to identifiable objects. Rvalues evaluate to temporary values.
Value Categories and Operators
Unless specified otherwise, operators expect rvalue operands. For instance, binary operator+ requires rvalues:
#include <iostream>
int main()
{
std::cout << 8 + 12; // Both 8 and 12 are rvalues, operator+ returns rvalue
return 0;
}
Literals 8 and 12 are rvalues. The operator+ uses them to return the rvalue 20.
Now we can explain why score = 100 works but 100 = score fails: assignment requires a modifiable lvalue on the left and an rvalue on the right. The second assignment fails because 100 (an rvalue) appears on the left.
int main()
{
int score{};
// Assignment needs: modifiable lvalue (left), rvalue (right)
score = 100; // Valid: score is modifiable lvalue, 100 is rvalue
100 = score; // Error: 100 is rvalue, score is modifiable lvalue
return 0;
}
Lvalue-to-Rvalue Conversion
Given that assignment expects an rvalue on the right, why does this work?
int main()
{
int width{10};
int height{20};
width = height; // height isn't an rvalue, yet this works
return 0;
}
When an rvalue is expected but an lvalue is provided, the lvalue undergoes lvalue-to-rvalue conversion. The lvalue is evaluated to produce its value (an rvalue).
In this example, lvalue height converts to rvalue 20, which is then assigned to width.
Core Understanding
Lvalues implicitly convert to rvalues, so lvalues work anywhere rvalues are expected. Rvalues do not implicitly convert to lvalues.
Consider another example:
int main()
{
int base{7};
base = base * 2;
return 0;
}
The variable base appears twice with different meanings. On the left (where an lvalue is needed), base is an lvalue referencing the variable. On the right, base undergoes lvalue-to-rvalue conversion, producing 7 for the multiplication. The operator* returns rvalue 14, which is assigned to base.
Distinguishing Lvalues from Rvalues
Determining whether an expression is an lvalue or rvalue takes practice. Here's a rule of thumb:
Tip
- Lvalues: Evaluate to functions or identifiable objects persisting beyond the expression
- Rvalues: Evaluate to values, including literals and temporary objects not persisting beyond the expression
For comprehensive lists, consult the C++ reference documentation.
You can also write code to determine value categories:
#include <iostream>
#include <string>
// T& prefers lvalues
template <typename T>
constexpr bool is_lvalue(T&)
{
return true;
}
// T&& prefers rvalues
template <typename T>
constexpr bool is_lvalue(T&&)
{
return false;
}
// Helper macro for printing
#define PRINT_CATEGORY(expr) { std::cout << #expr << " is " << (is_lvalue(expr) ? "lvalue\n" : "rvalue\n"); }
int getValue() { return 42; }
int main()
{
PRINT_CATEGORY(42); // rvalue
PRINT_CATEGORY(getValue()); // rvalue
int data{42};
PRINT_CATEGORY(data); // lvalue
PRINT_CATEGORY(std::string{"Hi"}); // rvalue
PRINT_CATEGORY("Hi"); // lvalue
PRINT_CATEGORY(++data); // lvalue
PRINT_CATEGORY(data++); // rvalue
}
Output:
42 is rvalue
getValue() is rvalue
data is lvalue
std::string{"Hi"} is rvalue
"Hi" is lvalue
++data is lvalue
data++ is rvalue
This uses overloaded functions: one with an lvalue reference parameter (preferred for lvalues), one with an rvalue reference (preferred for rvalues). The selected function reveals the value category.
Notice that operator++ results depend on usage: prefix (lvalue) versus postfix (rvalue).
Advanced note: C-style string literals are lvalues for backward compatibility with C. Arrays (which C-style strings are) decay to pointers, requiring an lvalue with an address.
Now that we understand lvalues, we're ready for our first compound type: the lvalue reference!
Summary
Expression properties: All C++ expressions have two properties: a type (determined at compile-time for type checking) and a value category (determined by what the expression evaluates to).
Value categories: Pre-C++11 categories are lvalue and rvalue. C++11 added glvalue, prvalue, and xvalue for move semantics (covered in future lessons).
Lvalues: Expressions evaluating to identifiable objects or functions with identity. Can be accessed via identifiers, references, or pointers. Have lifetimes exceeding a single expression. Come in modifiable (non-const) and non-modifiable (const) varieties.
Rvalues: Expressions evaluating to values without identity. Exist only within the expression's scope. Include literals (except C-style strings) and temporary values from operations or function returns.
Operator expectations: Most operators expect rvalue operands. Assignment requires modifiable lvalue on left and rvalue on right.
Lvalue-to-rvalue conversion: When rvalue expected but lvalue provided, lvalue automatically converts to rvalue by evaluating to its value. This enables lvalues to work anywhere rvalues are expected.
Rvalues do not convert to lvalues: Rvalues cannot be used where lvalues are required (e.g., left side of assignment).
Practical distinction:
- Lvalues: Evaluate to functions or identifiable objects persisting beyond the expression
- Rvalues: Evaluate to literals or temporary objects not persisting beyond the expression
Understanding value categories is essential for working with references, understanding assignment behavior, and later learning move semantics and perfect forwarding.
Understanding Lvalues and Rvalues - Quiz
Test your understanding of the lesson.
Practice Exercises
Value Categories: Lvalues and Rvalues
Understand the fundamental difference between lvalues and rvalues. Learn why some expressions can be assigned to and others cannot, and how lvalue-to-rvalue conversion works.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!