Skip to content
Home
C++ Move Semantics and Perfect Forwarding

C++ Move Semantics and Perfect Forwarding

C/C++ C/C++ 10 min read 1963 words Intermediate ExcellentWiki Editorial Team

Before C++11, passing objects in C++ meant either copying (expensive for large objects) or passing pointers (error-prone lifetime management). Move semantics changed this by allowing resources to be transferred from temporary objects without copying. Combined with perfect forwarding, it enables efficient generic code that preserves value category.

This guide covers rvalue references, move constructors, std::move, perfect forwarding, and practical performance patterns.

Lvalues and Rvalues

Every expression in C++ has a value category. The two fundamental categories are:

  • lvalue — An expression that refers to a memory location. It has an address and persists beyond a single expression.
  • rvalue — A temporary value that does not persist beyond the expression that creates it.
int x = 42;    // x is an lvalue, 42 is an rvalue
int y = x;     // y and x are lvalues
int z = x + y; // x + y is an rvalue (temporary)

Rvalue References

An rvalue reference (&&) binds only to rvalues:

void process(int& arg) {   // lvalue reference
    std::cout << "lvalue\n";
---

void process(int&& arg) {  // rvalue reference
    std::cout << "rvalue\n";
---

int x = 42;
process(x);      // lvalue overload
process(42);     // rvalue overload
process(std::move(x));  // rvalue overload (cast)

std::move does not move anything — it casts an lvalue to an rvalue reference, enabling the move overload.

Move Constructors and Move Assignment

The Problem with Copies

class Buffer {
    int* data;
    size_t size;
public:
    // Constructor
    explicit Buffer(size_t n) : size(n), data(new int[n]) {}

    // Copy constructor — expensive deep copy
    Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
        std::copy(other.data, other.data + size, data);
    }

    // Destructor
    ~Buffer() { delete[] data; }
---;

Buffer create_buffer() {
    Buffer b(1000000);
    // Fill b...
    return b;  // Before move semantics: deep copy
---

Before move semantics, returning large objects triggered a copy. Compilers could optimize with NRVO (Named Return Value Optimization), but not always.

Move Constructor

class Buffer {
    int* data;
    size_t size;
public:
    explicit Buffer(size_t n) : size(n), data(new int[n]) {}

    // Copy constructor
    Buffer(const Buffer& other)
        : size(other.size), data(new int[other.size]) {
        std::copy(other.data, other.data + size, data);
    }

    // Move constructor — steal resources
    Buffer(Buffer&& other) noexcept
        : data(other.data), size(other.size) {
        other.data = nullptr;   // Leave source in valid state
        other.size = 0;
    }

    // Move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data;            // Free existing resources
            data = other.data;        // Steal pointer
            size = other.size;
            other.data = nullptr;     // Leave source empty
            other.size = 0;
        }
        return *this;
    }

    ~Buffer() { delete[] data; }
---;

The move constructor takes an rvalue reference, copies the pointer, and nullifies the source. No deep copy of a million integers — just two pointer assignments. The noexcept specification is important: standard containers like std::vector use move semantics only if the move operation is noexcept.

The Rule of Five

Any class that manages a resource should define or delete all five special member functions:

class Resource {
public:
    ~Resource();
    Resource(const Resource&);            // Copy constructor
    Resource& operator=(const Resource&); // Copy assignment
    Resource(Resource&&) noexcept;        // Move constructor
    Resource& operator=(Resource&&) noexcept; // Move assignment
---;

If you define a custom destructor, copy constructor, or copy assignment, you likely need all five. Or use the rule of zero: let RAII wrapper classes handle resources.

When Move Is Used

Standard Library Containers

std::vector<std::string> v;
std::string s = "hello world this is a long string";

v.push_back(s);                 // Copy — s remains valid
v.push_back(std::move(s));      // Move — s is now empty
// s is in a valid but unspecified state

std::vector reallocates when it grows. With move semantics, elements are moved instead of copied during reallocation:

std::vector<BigObject> v;
v.reserve(2);
v.emplace_back(/* ... */);
v.emplace_back(/* ... */);

v.emplace_back(/* ... */);  // Triggers reallocation — moves elements

Returning from Functions

// RVO (Return Value Optimization) or move
std::vector<int> create_large_vector() {
    std::vector<int> result(1000000);
    // Fill result...
    return result;  // No copy — RVO or implicit move
---

auto v = create_large_vector();  // Efficient

The compiler first attempts RVO (eliding the copy/move entirely). If RVO does not apply, it falls back to an implicit move.

std::swap with Move

template <typename T>
void my_swap(T& a, T& b) {
    T temp = std::move(a);  // Move a into temp
    a = std::move(b);       // Move b into a
    b = std::move(temp);    // Move temp into b
---

For types like std::string or std::vector, swap becomes O(1) — just pointer swaps.

Perfect Forwarding

Perfect forwarding preserves the value category (lvalue/rvalue) of arguments passed through a function:

template <typename T>
void wrapper(T&& arg) {
    // Forward with perfect forwarding
    target_function(std::forward<T>(arg));
---

Why Forwarding Matters

void process(int& x)  { std::cout << "lvalue\n"; }
void process(int&& x) { std::cout << "rvalue\n"; }

template <typename T>
void bad_forward(T&& arg) {
    process(arg);  // Always calls lvalue overload — arg is named
---

template <typename T>
void good_forward(T&& arg) {
    process(std::forward<T>(arg));  // Preserves value category
---

int x = 42;
good_forward(x);       // lvalue
good_forward(42);      // rvalue
good_forward(std::move(x));  // rvalue

Universal References (Forwarding References)

T&& in a deduced context is a forwarding reference, not an rvalue reference:

template <typename T>
void f(T&&);          // Forwarding reference

auto&& x = expr;      // Forwarding reference (decltype(auto) style)

template <typename T>
void g(std::vector<T>&&);  // Rvalue reference — NOT forwarding

The distinction: T&& in a deduced template parameter is a forwarding reference. T&& in a non-deduced context (like std::vector<T>&&) is an rvalue reference.

std::forward Implementation

template <typename T>
T&& forward(std::remove_reference_t<T>& arg) noexcept {
    return static_cast<T&&>(arg);
---

When T is int&, T&& collapses to int& (lvalue). When T is int, T&& is int&& (rvalue). Reference collapsing rules make this work:

| T | T&& | Result | |

Rvalue References in Depth

The rvalue reference (&&) is the language feature that enables move semantics. It binds exclusively to rvalues — temporary objects or objects explicitly cast with std::move. This allows the compiler to distinguish between “I want a copy” (lvalue) and “I want to steal resources” (rvalue):

void process(std::vector<int> &&data) {
    // data is an rvalue reference — caller doesn't need it anymore
    // We can steal its internal buffer
---

std::vector<int> v(1000000);
process(std::move(v));  // v's buffer is moved, not copied
// v is now empty but valid

After a move, the source object is left in a valid but unspecified state. It can be destroyed or assigned to, but its contents should not be assumed.

Move Constructors and Move Assignment

A move constructor transfers resources from a source object to a new object without copying:

class Buffer {
    char *data;
    size_t size;
public:
    // Move constructor
    Buffer(Buffer &&other) noexcept
        : data(std::exchange(other.data, nullptr))
        , size(std::exchange(other.size, 0)) {}

    // Move assignment
    Buffer &operator=(Buffer &&other) noexcept {
        if (this != &other) {
            delete[] data;                    // Release current resources
            data = std::exchange(other.data, nullptr);  // Steal
            size = std::exchange(other.size, 0);
        }
        return *this;
    }
---;

The noexcept specifier is critical — std::vector will only use the move constructor when reallocating if it is noexcept. Otherwise, it falls back to copying to maintain the strong exception guarantee.

When Move Falls Back to Copy

Not all types benefit from move semantics. Fundamental types (int, double, pointers) and types without dynamically allocated resources are copied even when passed through std::move. The move is no more efficient than a copy for these types. Move semantics provide the greatest benefit for types that manage heap memory, file handles, or other dynamically acquired resources.

Perfect Forwarding

Perfect forwarding preserves the value category of arguments through template functions:

template<typename T, typename... Args>
std::unique_ptr<T> make_wrapper(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
---

std::forward conditionally casts to an rvalue reference — if the original argument was an rvalue, the forwarded argument is also an rvalue. This enables factory functions and wrapper templates to preserve move semantics through multiple layers of function calls.

FAQ

What is the difference between malloc and new in C++? malloc allocates raw memory without calling constructors; new allocates memory and calls the constructor. In C++, prefer new for objects. free vs delete follows the same pattern — delete calls the destructor.

How do I prevent memory leaks in C/C++? Use RAII (Resource Acquisition Is Initialization) in C++ — smart pointers like std::unique_ptr and std::shared_ptr automatically free memory. In C, always pair every malloc with a free and use tools like Valgrind or AddressSanitizer to detect leaks.

What is undefined behavior in C/C++? Undefined behavior occurs when code performs operations that the language standard does not define — dereferencing a null pointer, buffer overflow, signed integer overflow. The compiler may generate any code, including unexpected results or crashes.

Should I learn C or C++ first? Learn C first if you want to understand low-level memory and system programming. Learn C++ first if you want object-oriented features and the STL. Both are valuable; C++ builds on C concepts.

What is the difference between a header file and a source file? Header files (.h) declare interfaces — function prototypes, class definitions, macros. Source files (.c or .cpp) implement the declarations. Headers are #included; source files are compiled separately and linked.

—|—–|——–| | int& | int& && | int& | | int&& | int&& && | int&& | | int | int&& | int&& |

Reference Collapsing Rules

using Lref = int&;
using Rref = int&&;

int x = 42;
Lref&  r1 = x;   // int&  — lvalue ref to lvalue ref
Lref&& r2 = x;   // int&  — lvalue ref to rvalue ref
Rref&  r3 = x;   // int&  — rvalue ref to lvalue ref
Rref&& r4 = 42;  // int&& — rvalue ref to rvalue ref

The rule: & always wins. An && followed by & collapses to &. Only && + && = &&.

Practical Patterns

Emplace Back

std::vector<std::pair<std::string, int>> v;

// Constructs pair<string, int>, then copies/moves into vector
v.push_back(std::pair<std::string, int>("hello", 42));

// Constructs pair directly in place — no temporary
v.emplace_back("hello", 42);

emplace_back forwards its arguments to the element’s constructor, avoiding the temporary object entirely.

Move-Only Types

std::unique_ptr<int> ptr = std::make_unique<int>(42);
std::vector<std::unique_ptr<int>> v;

// v.push_back(ptr);    // Error: unique_ptr is not copyable
v.push_back(std::move(ptr));  // OK: transfer ownership
// ptr is now nullptr

Move-only types must be moved, not copied. This enforces unique ownership.

Factory Functions

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

Perfect forwarding preserves argument value categories through variadic templates.

Performance Considerations

When Move Is Not Faster

  • Small typesint, double, char — copying is as fast as moving
  • Trivially copyable typesstd::array<int, 3> — memcpy equals move
  • SSO strings — Short string optimization means small strings (< 15 chars) are copied even with std::move

Move Semantics with Compiler Optimizations

// RVO (guaranteed in C++17)
auto obj = createObject();  // No copy, no move — constructed in place

// NRVO (optional)
Buffer b = createBuffer();  // May elide copy/move entirely

C++17 guarantees copy elision for prvalues. NRVO is still optional but widely implemented.

Best Practices

  • Mark move operations noexcept — Containers like std::vector require noexcept moves for optimal reallocation behavior
  • Leave moved-from objects in a valid state — Typically empty or default-constructed. Document the post-condition.
  • Use std::move on expensive-to-copy types — Strings, vectors, and other heap-allocated types benefit. Do not use std::move on small types.
  • Do not use std::move on const objectsconst T&& will bind to the copy constructor, not the move constructor.
  • Prefer std::forward for templates — Use std::move for concrete types, std::forward for template parameters.

Summary

Move semantics eliminate expensive deep copies for temporary objects. Rvalue references distinguish between modifiable lvalues and temporaries. Move constructors transfer resources by pointer swap instead of deep copy. Perfect forwarding with std::forward preserves value categories through generic code. The result is C++ code that is both safer and faster — no raw pointer passing, no unnecessary copies, and compile-time guarantees about resource ownership.

See also: C vs C++: Key Differences and When to Use Which.

See also: C++ File I/O: Reading and Writing Files.

Section: C/C++ 1963 words 10 min read Intermediate 756 articles in section Report inaccuracy Back to top