value categories


Every expression in C++ has a value category. Modern C++ uses three primary categories:

evolution of value categories

The usage of l and r in the naming came from the idea of the left and right hand side of an assignment statement. In C we had code like this:

  
    int i;
    i = 42; // ok
    42 = i; // error
  

From this basic example we can deduce what types of things can be on the left vs right side of an assignment statement. This gave a way of categorizing expressions either as l-values or r-values. From here we can see what types of operations are avilable for different value categories:

  
    int* p = &i; // getting the memory address of an l-value is fine
    int* q = &42 // error, you can't get the memory address of an r-value
  

But in modern c++ this is no longer this case since assignment position no longer determines the value category, because the following code valid:

  
    std::string s;
    std::move(s) = "hello"; // valid it acts like an l-value
    // if std::move(...) acts like an l-value then we can get the reference of it right?
    auto sp = &std::move(s); // error 
  

The above example proves that with c++ std::move(...)'s valid category is neither an l-value nor an r-value.

Pure R-value (prvalue)

A prvalue is a value that does not have identity. It typically represents a temporary or a computed value. Evaluating a prvalue initializes an object but does not refer to an existing object with identity.

Examples:

  3
x + y
std::string("hello")
  

Expiring Value (xvalue)

An xvalue is an expression that has identity but whose resources can be reused (about to expire). Xvalues arise from certain rvalue expressions, such as a function returning an rvalue reference.

Examples:

  
std::move(obj)       // object with identity but eligible for reuse
f().member           // if f() returns T&&

L-Value (lvalue)

An lvalue refers to an object with identity. It has a stable location in memory and cannot bind to rvalue references. Most named variables and objects are lvalues.

Examples:

  
x           // named variable
arr[i]      // subscript produces lvalue
*p          // dereferencing a pointer
  

Universal/Forwarding Reference

Suppose we have the following code:


#include <iostream>
#include <string>
#include <utility> // for std::move

// 1. Accept const lvalue reference: we just want to read it
void log_internal(const std::string& msg) {
    std::cout << "Logging const lvalue reference: " << msg << "\n";
}

// 2. Accept non-const lvalue reference: we might modify it
void log_internal(std::string& msg) {
    msg += " [logged]";
    std::cout << "Logging non-const lvalue reference: " << msg << "\n";
}

// 3. Accept rvalue reference: we can move it
void log_internal(std::string&& msg) {
    std::cout << "Logging rvalue reference: " << msg << "\n";
}

int main() {
    std::string s1 = "Hello";
    const std::string s2 = "World";

    log_internal(s1);        // calls non-const lvalue
    log_internal(s2);        // calls const lvalue
    log_internal("Temp!");   // calls rvalue

    return 0;
}

Now we realize that we want to be able to log anything, not just strings. A naive approach would be to create three different template signatures:


template <typename>
void log_internal(const T& msg) { ... }

template <typename>
void log_internal(T& msg) { ... }

template <typename>
void log_internal(T&& msg) { ... }

The problem with this approach is that templates are supposed to reduce boilerplate, but here we are duplicating code three times. This goes against the very purpose of templates.

The proper way: Universal/Forwarding References

Instead of writing three templates, we can use a universal reference and std::forward to perfectly forward the argument to the correct internal function. Here's an example:


#include <iostream>
#include <string>
#include <utility> // for std::forward

// Internal implementations
void log_internal_impl(const std::string& msg) {
    std::cout << "Logging const lvalue reference: " << msg << "\n";
}

void log_internal_impl(std::string& msg) {
    msg += " [logged]";
    std::cout << "Logging non-const lvalue reference: " << msg << "\n";
}

void log_internal_impl(std::string&& msg) {
    std::cout << "Logging rvalue reference: " << msg << "\n";
}

// Forwarding template
template <typename>
void log_internal(T&& msg) {             // universal reference
    log_internal_impl(std::forward<t>(msg));
}

int main() {
    std::string s1 = "Hello";
    const std::string s2 = "World";

    log_internal(s1);        // calls non-const lvalue
    log_internal(s2);        // calls const lvalue
    log_internal("Temp!");   // calls rvalue

    return 0;
}

Why this works: