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.