Skip to content

⚖️ Lvalues & Rvalues — concise tutorial

What are lvalues and rvalues?

Lvalues identify objects with identity (you can take their address). Rvalues are temporary values or expressions that don't have a persistent identity. Modern C++ refines these into:

  • prvalue: pure rvalue (e.g. 42, std::string("hi")).
  • xvalue: expiring value (e.g. std::move(x), function returning T&&).
  • glvalue: general lvalue (either lvalue or xvalue).

Value categories determine which references and overloads an expression can bind to and are the foundation for move semantics and perfect forwarding.


Why it matters

  • Enables efficient resource transfer (move semantics) instead of expensive copies.
  • Affects overload resolution and template behavior (forwarding references).
  • Helps write correct, performant APIs: accept by value, const ref, or universal reference depending on intent.

Basic usage (short)

  • T& binds to lvalues only.
  • T&& (rvalue reference) binds to rvalues/xvalues and is used for moves and perfect forwarding.
  • std::move(x) casts x to an xvalue — it enables moving but does not move by itself.
  • std::forward<T>(t) preserves the value category in templates (perfect forwarding).

Example quick snippets:

int a = 1;        // 'a' is an lvalue
int& la = a;      // OK: lvalue reference
int&& ra = 2;     // OK: rvalue reference binds to prvalue
int&& ra2 = std::move(a); // xvalue

Minimal move example

#include <string>
#include <utility>

struct Buffer { std::string data; };

struct Holder {
    std::string s;
    Holder(std::string str) : s(std::move(str)) {}
    Holder(Holder&& other) noexcept : s(std::move(other.s)) {}
    Holder& operator=(Holder&& other) noexcept { s = std::move(other.s); return *this; }
};

Holder makeHolder(){ return Holder("hello"); }
// caller: Holder h = makeHolder(); // moved or elided

Notes: mark move operations noexcept when possible to allow containers (e.g., std::vector) to prefer moves during reallocation.


Perfect forwarding (short)

Use forwarding references in templates and std::forward to preserve lvalue/rvalue-ness:

#include <utility>
#include <string>

void consume(const std::string&); // lvalue overload
void consume(std::string&&);      // rvalue overload

template<class T>
void wrapper(T&& arg){ consume(std::forward<T>(arg)); }

std::string s = "hi";
wrapper(s);                 // lvalue overload called
wrapper(std::string("x")); // rvalue overload called

Common pitfalls & tips

  • std::move on an object you still need — the object enters a valid but unspecified state.
  • Missing noexcept on move constructors can force containers to copy instead of move during growth.
  • Named rvalue references inside templates are lvalues; use std::move/std::forward deliberately.
  • Don't return references to local (stack) variables — leads to dangling references.
  • For small trivially-copyable types, moving offers no benefit; prefer simplicity.

Practical rule-of-thumb: design APIs with clear ownership semantics — accept by const T&, T, or T&& depending on whether you need a copy, a view, or to take ownership.


How to try / compile

Save examples into a file (one snippet per file) and compile with a modern compiler supporting C++17/20:

g++ -std=c++17 example.cpp -O2 -Wall -Wextra -o example
clang++ -std=c++17 example.cpp -O2 -Wall -Wextra -o example

If you experiment with coroutines or other newer features, use -std=c++20.


Exercises

  1. Implement a small RAII Buffer that manages a heap array; add copy and move constructors/assignments and push instances into std::vector<Buffer> to observe moves vs copies.
  2. Write a templated emplace_and_consume that constructs an object from forwarded args and consumes it; verify overloads for lvalues and rvalues.
  3. Instrument move/copy constructors and observe how std::vector behaves when noexcept is omitted vs present.

References

  • cppreference: https://en.cppreference.com/w/cpp/language/value_category
  • Herb Sutter and C++ core guidelines on move semantics and forwarding

If you'd like, I can:

  • add runnable examples under docs/Cpp/examples/ and a small Makefile, or
  • expand the section on lifetime of temporaries with diagrams, or
  • add tests demonstrating std::vector growth behavior with/without noexcept on moves.

Tell me which you'd like next.


Value categories overview

  • lvalue: an expression that identifies a persistent object (has identity). Example: a variable int x — the expression x is an lvalue.
  • prvalue (pure rvalue): a temporary value that initializes objects or computes values, e.g. 42, std::string("hi") (before C++17 prvalues materialize differently).
  • xvalue (expiring value): an object whose resources can be reused (e.g. result of std::move(x) or a function returning T&&). It's an rvalue but represents an object with identity that is near end-of-life.
  • glvalue: general lvalue (either lvalue or xvalue). In the C++ taxonomy glvalue ∪ prvalue = rvalue?

Simple mapping:

  • expression x (where x is a named variable) -> lvalue
  • literal 42, "abc" -> prvalue
  • std::move(x) -> xvalue
  • result of a+b -> prvalue

Why it matters: value categories determine which overloads and reference types an expression can bind to, and they are the foundation for move semantics and perfect forwarding.

Lvalue and rvalue references

  • Lvalue reference: T& — binds to lvalues only.
  • Rvalue reference: T&& — can bind to rvalues and xvalues (usable for move semantics and overload resolution).

Examples:

int a = 1;    // 'a' is an object (lvalue)
int& ra = a;  // OK: lvalue reference
// int& rb = 2; // error: can't bind lvalue ref to prvalue
int&& rr = 2; // OK: rvalue reference binds to prvalue
int&& rr2 = std::move(a); // OK: std::move(a) is an xvalue

Reference collapsing rules (important for templates):

  • T& & -> T&
  • T& && -> T&
  • T&& & -> T&
  • T&& && -> T&&

Move semantics and std::move

Move semantics let you transfer (steal) resources from temporaries or objects you no longer need, avoiding expensive deep copies.

  • Provide a move constructor T(T&&) and move assignment T& operator=(T&&) when your type manages resources (heap memory, file handles, etc.).
  • std::move(x) casts x to an rvalue (xvalue) to enable moving; it does not move by itself.

Example: a minimal movable class

#include <utility>
#include <string>

struct Buffer {
    std::string data;
    Buffer() = default;
    Buffer(const Buffer&) = default;            // copy
    Buffer(Buffer&& other) noexcept : data(std::move(other.data)) {} // move
    Buffer& operator=(Buffer&& other) noexcept { data = std::move(other.data); return *this; }
};

// Usage
Buffer make_buffer(){ Buffer b; b.data = "hello"; return b; }
Buffer b2 = make_buffer();         // move (or elide)
Buffer b3 = std::move(b2);        // move from named object

Notes:

  • Mark move operations noexcept when possible to enable some containers (e.g., std::vector) to use move instead of copy during reallocations.
  • After moving, the moved-from object must be left in a valid but unspecified state.

Perfect forwarding and std::forward

Perfect forwarding preserves the value category of arguments when you forward them through template wrappers.

  • Use a forwarding reference (formerly called universal reference) T&& in a template parameter: template<class T> void f(T&& t).
  • Use std::forward<T>(t) to forward t preserving lvalue/rvalue-ness.

Example:

#include <utility>
#include <string>

void consume(const std::string& s) { /* lvalue overload */ }
void consume(std::string&& s) { /* rvalue overload */ }

template<class T>
void wrapper(T&& arg) {
    consume(std::forward<T>(arg)); // forwards lvalue as lvalue, rvalue as rvalue
}

std::string s = "hi";
wrapper(s);            // calls consume(const std::string&)
wrapper(std::string("x")); // calls consume(std::string&&)

Common pitfalls

  • Using std::move on an object you still need — leaves it in a valid but unspecified state.
  • Binding rvalue references to temporaries in ways that extend lifetime incorrectly (be careful with returning references).
  • Forgetting noexcept for move constructors/assignments can degrade performance (some containers fall back to copy during reallocation).
  • Overloading surprises: adding both T& and T&& overloads may change overload resolution; named variables are lvalues even if their type is T&& inside templates.
  • Dangling references: never return references to local variables.

Guidelines and best practices

  • Prefer value semantics and rely on move semantics for performance.
  • Implement move operations only when your type manages resources; otherwise the compiler-generated ones are usually fine.
  • Use = default for copy/move when appropriate.
  • Mark move constructors and move assignment noexcept when they cannot throw.
  • Use std::move only when you intend to transfer ownership; prefer not to overuse it for small trivially-copyable types.
  • Use perfect forwarding in generic wrapper functions to preserve value categories.

Examples

  1. Move constructor example (complete):
#include <string>
#include <iostream>

struct Holder {
    std::string s;
    Holder(std::string str) : s(std::move(str)) {}
    Holder(const Holder&) = default;
    Holder(Holder&& other) noexcept : s(std::move(other.s)) { std::cout << "moved\n"; }
};

int main(){
    Holder a("hello");
    Holder b = std::move(a); // prints "moved"
}
  1. Perfect forwarding example:
#include <utility>
#include <string>
#include <iostream>

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

template<class T>
void wrapper(T&& t) { consume(std::forward<T>(t)); }

int main(){
    std::string s = "abc";
    wrapper(s);                  // lvalue
    wrapper(std::string("xyz")); // rvalue
}

Compile (recommended):

g++ -std=c++17 -O2 -Wall -Wextra examples.cpp -o examples

Exercises

  1. Implement a small Buffer class that manages a heap array, add copy and move constructors and assignments, and demonstrate moves in a std::vector<Buffer>.
  2. Write a template wrapper make_and_consume that constructs an object from forwarded constructor args and passes it to a consumer; test with lvalues and rvalues.
  3. Show how a missing noexcept on move constructor affects std::vector when growing (inspect whether copies or moves are used by instrumenting constructors).

References

  • C++ standard sections on value categories and expressions
  • cppreference: https://en.cppreference.com/w/cpp/language/value_category
  • Herb Sutter: GotW / talks about rvalue references and move semantics