C++ / a working model

48 / 163   ·   C++11   ·   9 min

Value categories and std::move: conversion is not a move

Keep this sentence

Lvalues, xvalues, and prvalues describe expressions, not permanent labels that variables own. std::move only converts an expression into a form that can participate in move overloads; whether resources actually transfer and what state the source is left in depend on the called type's contract.

In this lesson
  1. Analyze value categories through expressions
  2. The receiver performs the move
  3. When not to move on purpose
  4. Example
  5. Exercise

Analyze value categories through expressions

C++11 subdivides expressions into lvalues, xvalues, and prvalues. Lvalues and xvalues together are glvalues, emphasizing object identity; prvalues and xvalues together are rvalues, which can bind to rvalue references. Do not judge only by "which side of the equals sign," because literals, function calls, and reference expressions have their own rules.

A named variable expression is usually an lvalue, even when the variable's declared type is T&&. In the example, r is an rvalue-reference variable, but passing r to the overloaded function selects the lvalue version; writing std::move(r) then yields an xvalue. That distinguishes declared type from the category of the expression used.

The receiver performs the move

std::move(x) is essentially a conversion that preserves the underlying cv qualifications; it does not transfer resources and does not independently change x. Only after the result is given to a constructor or assignment operator can overload resolution call a move operation. For a type that owns an exclusive resource, that step can hand resource management to the target.

The example uses unique_ptr to verify a definite post-move contract: the target still points to the original integer, and the source becomes a null pointer. Do not generalize that conclusion to every container; many standard-library types only guarantee that the object remains valid with an unspecified value after a move. You may then destroy it, assign a new value, and perform operations that meet their preconditions, but you should not assert that the original contents remain or that the object is necessarily empty.

When not to move on purpose

Applying std::move to a const object usually yields a const rvalue reference, while a typical move constructor needs a modifiable T&&, so copy construction may still be selected. Design const from whether the object is allowed to give up its resources, rather than piling conversions at the call site to force a move.

When returning a local value, usually write return local and let the compiler use named return-value optimization or implicit-move rules. Writing return std::move(local) can inhibit NRVO. Some C++17 prvalue return paths have guaranteed elision, but that does not let you claim that every return performs a move, or that every move is cheaper than a copy.

Pitfalls

  • std::move does not strip const; if the receiver has no matching move overload, the converted expression may still trigger a copy.
  • Reading the source object's former business value after a move is not a general contract; in particular, do not generalize unique_ptr's emptied guarantee to string.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <cassert>
#include <iostream>
#include <memory>
#include <utility>

int category(int&) { return 1; }
int category(int&&) { return 2; }

int main() {
    int n = 5;
    int&& r = std::move(n);
    assert(category(r) == 1);
    assert(category(std::move(r)) == 2);
    assert(n == 5);
    std::unique_ptr<int> source(new int(42));
    int* original = source.get();
    std::unique_ptr<int> target = std::move(source);
    assert(!source);
    assert(target.get() == original && *target == 42);
    std::cout << category(r) << ' ' << category(std::move(r)) << ' ' << *target << '\n';
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread modern-move.cpp -o example && ./example

Expected result

1 2 42

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

After int&& r = std::move(n), does assigning 8 to r change n? Does this step construct a new int?

Show a reference answer

Yes, it changes n, because r refers directly to n and does not construct a second int. std::move(n) changes the expression's category, not storage identity. Only the operation that receives that expression decides whether a target object is created and how its contents are transferred.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog