C++ Value Categories: lvalues, rvalues, and std::move

5 minute read

Published:

Why Value Categories Matter

Every C++ expression has a type and a value category. The value category affects:

  • which reference can bind to the expression,
  • which overload is selected,
  • whether an object may be moved from,
  • when a temporary object is materialized,
  • whether copy elision can construct a result directly.

The terms describe expressions, not permanent properties of objects. The same object can be reached through expressions with different value categories.

The Five Categories

Modern C++ defines five related categories:

CategoryHas identity?Resources may be reused?Typical example
lvalueYesNoA named variable
xvalueYesYesstd::move(variable)
prvalueNo identity before materializationYesWidget{} or 2 + 3
glvalueYesDependsAn lvalue or xvalue
rvalueDependsYesA prvalue or xvalue

The useful relationships are:

glvalue = lvalue or xvalue
rvalue  = prvalue or xvalue

An xvalue belongs to both groups: it identifies an existing object, but that object’s resources may be reused.

Direct Examples

#include <string>
#include <utility>

int value = 10;
int* pointer = &value;

value;                          // lvalue: names an object
*pointer;                       // lvalue: identifies value through a pointer
std::string{"temporary"};      // prvalue: creates a temporary string
value + 5;                      // prvalue: computes a value
std::move(value);               // xvalue: identifies value as movable

An lvalue is not necessarily modifiable:

const int limit = 10;

// limit is an lvalue expression, but it is const.
// limit = 20;  // error

This is why “lvalue means something assignable on the left” is not a reliable definition.

Reference Binding Rules

References expose the practical difference between the categories.

#include <string>

std::string name = "vehicle";

std::string& mutable_alias = name;          // non-const lvalue reference
const std::string& read_only_alias = name;  // const lvalue reference
const std::string& temporary_alias =
    std::string{"temporary"};               // also binds to a temporary
std::string&& expiring_alias =
    std::string{"temporary"};               // rvalue reference
Reference typeWhat it normally binds toTypical purpose
T&Non-const lvalueMutable borrowing
const T&Lvalue or rvalueRead-only borrowing without copying
T&&RvalueMoving or forwarding

These rules are simplified for non-template code. A T&& inside a deduced function template may be a forwarding reference, which follows additional reference-collapsing rules.

A Named Rvalue Reference Is an lvalue

The type of a variable and the value category of an expression are different questions.

#include <string>
#include <utility>

void receive(const std::string& value);  // lvalue-friendly overload
void receive(std::string&& value);       // rvalue overload

void forwardToReceiver(std::string&& input) {
  receive(input);             // input is a named expression: lvalue
  receive(std::move(input));  // std::move(input) is an xvalue
}

Although input has type std::string&&, writing its name produces an lvalue expression. This prevents accidental repeated moves from named variables.

What std::move Does

std::move does not transfer memory or resources. It casts its argument to an xvalue so that move-aware overloads may be selected.

Conceptually:

#include <type_traits>

template <typename T>
std::remove_reference_t<T>&& move(T&& value) noexcept {
  return static_cast<std::remove_reference_t<T>&&>(value);
}

The receiving constructor or assignment operator performs the actual move:

#include <string>
#include <utility>

std::string source = "telemetry";
std::string destination = std::move(source);

Afterward, source remains alive and valid, but its value is generally unspecified. It can be destroyed or assigned a new value.

Overload Selection Example

#include <iostream>
#include <string>
#include <utility>

void inspect(const std::string&) {
  std::cout << "const lvalue reference\n";
}

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

int main() {
  std::string value = "data";

  inspect(value);             // const lvalue reference
  inspect(std::string{"x"}); // rvalue reference
  inspect(std::move(value));  // rvalue reference
}

The overload set makes the value category observable. std::move changes overload resolution; it does not guarantee that the called code will move anything.

Pointers, &, and &&

The symbols depend on context:

int value = 10;

int* pointer = &value;  // int*: pointer type; &value: address-of
int& alias = value;     // &: lvalue reference declarator
int&& temporary = 20;  // &&: rvalue reference declarator

*pointer = 30;          // *pointer: dereference; produces an lvalue
  • A pointer stores an address and may be null or reseated.
  • A reference is an alias and must be initialized when created.
  • An rvalue reference is still a reference; its main role is binding to rvalue expressions.
  • Outside a declaration, && may instead mean logical AND.

Returning Objects: Copy Elision and Move

Returning by value does not imply that a copy or move must occur.

class Report {};

Report createDirectly() {
  return Report{};  // guaranteed copy elision since C++17
}

Report createNamed() {
  Report report;
  return report;    // NRVO is permitted; move is the fallback
}

In createDirectly, the result object is constructed directly in the caller’s destination. No temporary Report needs to be copied or moved.

For a named local, compilers normally apply named return value optimization. If they do not, the return statement can use move construction when available. Writing return std::move(report); can prevent NRVO and should usually be avoided.

Interview Reconstruction

When reasoning about an expression, ask:

  1. Does this expression identify an existing object? If yes, it is a glvalue.
  2. May its resources be reused? If yes, it is an rvalue.
  3. Is it a named variable expression? Named variables are lvalues, even when their type is T&&.
  4. Did std::move appear? It creates an xvalue; the receiver decides whether a move occurs.
  5. Is a function returning a fresh prvalue? C++17 may construct the result directly.

Further Reading

Leave a Comment