Move semantics and perfect forwarding

C++
Author

Quasar

Published

December 23, 2025

Value Categories

C++ defines the following value categories:

  • lvalues: expressions for locations of long-living objects or functions. These objects have identity, persist in memory and are addressable.
  • prvalues: expressions for short-living values for initializations. prvalues themselves do not exist somewhere in memory, they do not denote objects. They are unmaterialized entities meant for initialization.
  • xvalue: A special location, representing a (long-living) object, whose resources/values are no longer needed and can be reused. The guts of this object can be stolen.

The moment a prvalue (conceptual temporary) becomes an xvalue(temporary object), it is called materialization. The temporary materialization conversion is usually an implicit prvalue-to-xvalue conversion.

Anytime a prvalue is used where a lvalue or xvalue is expected, a temporary object is created and initialized with the prvalue.

Rules for binding references

Define:

struct X{};
X v;
const X c;

void f(X);
void f(X&);
void f(const X&);
void f(X&&);
void f(const X&&);
  • A non-const lvalue reference X& takes only non-const lvalues.

  • A rvalue reference X&& takes only non-const rvalues.

  • A const lvalue reference const X& can take everything and serves as a fallback mechanism for move semantics.

  • A const rvalue reference takes both modifiable and const rvalues (e.g. std::move(c)).

  • A universal reference binds to all value categories and are usually used to forward arguments.

Overload resolution priority

Call f(X&) f(const X&) f(X&&) f(const X&&) f(T&&)
f(v) 1 3 no no 2
f(c) no 1 no no 2
f(X{}) no 4 1 3 2
f(std::move(v)) no 4 1 3 2
f(std::move(c)) no 3 no 1 2

Note that, universal reference is always the second best option.

Universal references and detail

Note that, when we declare arg as T&& (pronounced T-ref-ref) where T is function template type parameter, this is a universal(forwarding reference). It does not follow the rules of rvalue references.

template<typename T>
void foo(T&& arg){}

Reference collapsing rules

For a non-const object v and a const object c the type T and the type of arg is deduced as follows:

T T&& arg
foo(v) X& X& && X&
foo(c) const X& const X& && const X&
foo(X{}) X X && X&&
foo(std::move(v)) X&& X&& && X&&
foo(std::move(c)) const X&& const X&& && const X&&
  • X& & becomes X&.
  • X& && becomes X&.
  • X&& & becomes X&.
  • X&& && becomes X&&.

Copy elison

Copy elison omits copy and move constructors, resulting in zero-copy pass-by-value semantics.

Mechanics

The basic mechanics of Return Value Optimization(RVO) is as follows:

  • The caller allocates space on the stack for the return value, passes the address to the callee.
  • The callee constructs the result directly in that space.

In the below code snip,

#include <print>
#include <iostream>

struct X{
    X(double val) : m_val{val} { std::cout << "\n" << "constructed at " << this; }
    X(const X&){ std::println("X(const X&)"); }
    X(X&&){ std::println("X(X&&)"); }
    ~X(){ std::cout << "\n" << "destructed at " << this; }
    double m_val;
};

void f(X arg){
    std::cout << "\n" << "&arg = " << &arg;
}

X g(){
    X obj = X(10);  // copy elison initializing obj
                    // from temporary
    return obj;     // NRVO
}

X h(){
    return X(15);   // URVO
}

int main(){
    f(X(42));
    X v1{g()};  // Copy elison initializing v1 from the result of g()
    std::cout << "\n" << "&v1 = " << &v1;
    X v2{h()};  // Copy elison initializing v2 from the result of h()
    std::cout << "\n" << "&v2 = " << &v2;
    return 0;
}

Compiler Explorer

It is important to note that:

  • When a temporary(prvalue) is used to initialize an object, or function parameter, a copy is elided.
  • Inside a function, when a prvalue is returned by value, URVO(unnamed return value optimization) is performed.
  • Inside a function, when a lvalue is returned by value, NRVO(named return value optimization) is performed.

Challenge puzzle

Observe the below code snip. What is the output printed?

#include <iostream>
struct A {
    A(int x) : x(x) {}
    A(const A& a) { x = 1; }
    A(A&& a) { x = 2; }
    int x;
};

void foo(A t) {
    std::cout << t.x;
}

A bar(){
    A a(5);
    return a;
}

A foobar(){
    return A(6);
}

A&& baz(A&& arg){
    return std::move(arg);
}

int main() {
    A a(3);
    foo(a);
    foo(A(4));
    std::cout << bar().x;
    std::cout << foobar().x;
    A result{ baz(A(7)) };
    std::cout << result.x;
}

Compiler Explorer

For more such puzzles, visit getcracked.io