Phase 4 · Advanced C++ & Systems ProgrammingModule 20~60 min read

Value Categories, Move Semantics & Perfect Forwarding

Reason precisely about expression categories, reference collapsing, forwarding, elision, and lifetime-sensitive interfaces.

What you'll learn

Move-aware C++ depends on the category of an expression, not merely the type of an object. This lesson builds the precise model behind lvalues, prvalues, xvalues, reference collapsing, forwarding, copy elision, and lifetime-sensitive interfaces.

By the end, you'll be able to:

  • Classify expressions as lvalues, xvalues, or prvalues
  • Predict reference collapsing and value-category overload selection
  • Use std::move and std::forward only at ownership boundaries
  • Recognize copy elision, lifetime extension, and dangling reference traps

Expression value categories

An lvalue identifies an object whose identity remains available; an xvalue identifies an expiring object whose resources may be reused; a prvalue computes a value that can initialize an object. Glvalue means “has identity” and includes lvalues and xvalues. Rvalue includes xvalues and prvalues.

ExpressionCategoryReason
namelvalueA named object has persistent identity
42prvalueComputes an integer value
make_widget()prvalueReturned class value initializes a result
std::move(name)xvalueIdentifies name as eligible for reuse
*pointerlvalueIdentifies the pointed-to object

Key idea

A variable declared with T&& is still an lvalue expression whenever you use its name. Names have identity; use std::move or std::forward to recover a different category deliberately.

Reference binding and collapsing

An lvalue reference normally binds to lvalues. An rvalue reference binds to rvalues and can mark a source as transferable. When template substitution or a type alias forms a reference to a reference, collapsing rules produce one final reference type: any lvalue reference wins; only rvalue-reference plus rvalue-reference remains an rvalue reference.

CombinationCollapses to
T& &T&
T& &&T&
T&& &T&
T&& &&T&&

Category-sensitive overloads

Overloads can distinguish a read-only lvalue source from an expiring rvalue source. Avoid multiplying overloads without a real behavior difference; taking a small value by value is often simpler. For a sink that stores a string, a by-value parameter followed by one move gives callers a clear copy-or-move path.

overloads.cpp
#include <iostream>
#include <string>
#include <utility>

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

int main() {
    std::string name{"Ada"};
    inspect(name);
    inspect(std::string{"Bjarne"});
    inspect(std::move(name));
}

Watch out

After inspect(std::move(name)), the callee is permitted to move from name. Do not add std::move merely to silence a copy; it announces that the old value is no longer needed.

move versus forward

std::move unconditionally produces an xvalue. std::forward<T>preserves the original argument category when T&& is a forwarding reference— that is, an rvalue reference to a deduced, cv-unqualified template parameter.

factory.cpp
#include <memory>
#include <utility>

template <typename T, typename... Args>
std::unique_ptr<T> create(Args&&... args) {
    return std::make_unique<T>(std::forward<Args>(args)...);
}

struct Connection {
    Connection(const char* host, int port);
};

auto connection = create<Connection>("localhost", 8080);

Tip

Forward exactly once along the path that transfers an argument to its final consumer. Reading or forwarding the same potentially moved-from argument again is a design smell.

Copy elision and return values

Since C++17, many prvalues initialize their destination directly with no temporary move or copy. Named return value optimization can also construct a named local result in the caller. Return local values normally; adding std::move to the return can inhibit elision.

return_value.cpp
#include <string>
#include <vector>

struct Report {
    std::string title;
    std::vector<int> values;
};

Report build_report() {
    Report report{"Quarterly", {4, 7, 9}};
    return report; // NRVO may construct directly in the caller
}

Report empty_report() {
    return Report{"Empty", {}}; // guaranteed prvalue elision
}

Lifetime extension and dangling

Binding a temporary directly to a local const reference can extend that temporary to the reference's scope. The rule does not generally pass through return statements, reference members initialized from constructor parameters, or non-owning views. A returned reference or string_view must point into storage that outlives the caller's use.

lifetimes.cpp
#include <string>
#include <string_view>

std::string make_name() { return "Modern C++"; }

int main() {
    const std::string& safe{make_name()}; // temporary lifetime extends locally

    std::string owner{"persistent"};
    std::string_view view{owner};         // valid while owner remains alive and unchanged

    // std::string_view dangling{make_name()}; // temporary string dies immediately
}

Watch out

Non-owning views are cheap because they do not participate in lifetime management. Do not return a view of local data or store a view without a clear owner relationship.

Recap & quick check

Key takeaways

  • Value categories classify expressions: lvalues and xvalues have identity; prvalues compute values.
  • Reference collapsing makes any lvalue-reference component produce an lvalue reference.
  • std::move marks an expression reusable; std::forward preserves a deduced caller category.
  • Return values normally and let copy elision avoid unnecessary transfers.
  • Reference and view APIs must make owner lifetime relationships provable.

Quick check

1. What category is a named variable expression?

2. What does T& && collapse to?

3. What is std::forward for?

4. Why avoid return std::move(local)?

Next: Module 21 — Smart Pointers, Ownership Graphs & Allocators, where these transfer rules scale to complex resource structures.