Modern C++ Fundamentals: From Basics to Smart Pointers and Threads · Ownership, Templates, Errors and Concurrency

Move Semantics and the Rule of Five

Understand lvalues and rvalues, use std::move to transfer resources instead of copying them, and write move constructors that make large objects cheap to pass around.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A Buffer class that logs every copy and move, so you can see exactly when C++ copies data, when it moves it, and what std::move really does.

The idea

Copying a large object (a std::vector of a million elements) is expensive. Often the source is about to be destroyed anyway, so we could simply steal its internals. That is move semantics (C++11).

  • An lvalue has a name and an address (buffer). An rvalue is a temporary (make_buffer(), 5 + 3).
  • A move constructor Buffer(Buffer&& other) noexcept takes an rvalue reference, transfers the resource and leaves the source valid but empty.
  • std::move(x) doesn't move anything; it only casts x to an rvalue so the compiler picks the move constructor. After moving from an object, only assign to it or destroy it.
  • Return by value: returning a local usually moves it, or is optimized away completely (copy elision). Don't write return std::move(local);; it prevents that optimization.
  • noexcept on moves matters: std::vector only uses your move constructor when reallocating if it cannot throw.

The rule of five: if you define any of the destructor, copy constructor, copy assignment, move constructor or move assignment, define all five (or = default/= delete the rest). The rule of zero is better: hold standard types (std::vector, std::string, smart pointers) and write none of them.

The code

Counting copies and moves
#include <iostream>
#include <string>
#include <utility>
#include <vector>

class Buffer {
public:
    explicit Buffer(std::string name, std::size_t size = 4) : name_{std::move(name)}, data_(size, 0) {
        std::cout << "construct " << name_ << '\n';
    }
    Buffer(const Buffer& other) : name_{other.name_ + " (copy)"}, data_{other.data_} {
        std::cout << "copy " << other.name_ << '\n';
    }
    Buffer(Buffer&& other) noexcept : name_{std::move(other.name_)}, data_{std::move(other.data_)} {
        std::cout << "move " << name_ << '\n';
    }
    Buffer& operator=(const Buffer&) = default;
    Buffer& operator=(Buffer&&) noexcept = default;
    ~Buffer() = default;

    std::size_t size() const { return data_.size(); }
    const std::string& name() const { return name_; }

private:
    std::string name_;
    std::vector<int> data_;
};

Buffer make_buffer(const std::string& name) {
    Buffer local{name, 1000};
    return local;                      // moved or elided, never copied
}

void consume(Buffer b) { std::cout << "consuming " << b.name() << " of " << b.size() << " ints\n"; }

int main() {
    Buffer a{"alpha"};
    Buffer b = a;                      // copy: a is an lvalue
    Buffer c = std::move(a);           // move: std::move casts a to an rvalue
    std::cout << "a after move has " << a.size() << " ints; c has " << c.size() << '\n';

    Buffer d = make_buffer("beta");    // constructed directly, no copy or move printed
    consume(std::move(d));             // moved into the parameter
    consume(b);                        // copied into the parameter

    std::vector<Buffer> list;
    list.reserve(2);
    list.push_back(Buffer{"temp"});    // a temporary: moved into the vector
    list.emplace_back("built in place");   // constructed directly inside the vector
    std::cout << "vector holds " << list.size() << " buffers\n";
}

Output

construct alpha
copy alpha
move alpha
a after move has 0 ints; c has 4
construct beta
move beta
consuming beta of 1000 ints
copy alpha (copy)
consuming alpha (copy) (copy) of 4 ints
construct temp
move temp
construct built in place
vector holds 2 buffers

Run this on your computer — see “Get Started” for setup.

Notice that the moved-from a is empty, and that make_buffer prints no copy or move at all: the compiler built the object directly in d.

Common mistakes

  • Using an object after std::move. It's valid but unspecified; don't read its value. Reassign it before reuse.
  • Writing return std::move(local);. It blocks copy elision and can make code slower.
  • Forgetting noexcept on move operations. Containers then fall back to copying during reallocation.
  • Defining only some of the five special functions. A custom destructor with default copy leads to double frees; follow the rule of five or the rule of zero.
  • Thinking std::move moves. It only enables moving; the move constructor does the work.

Key points

  • Move semantics transfer resources from temporaries or objects you no longer need, avoiding copies.
  • std::move is a cast to an rvalue; the move constructor performs the transfer.
  • Mark move operations noexcept; return locals by value and let the compiler optimize.
  • Prefer the rule of zero; otherwise implement all five special member functions.

Further reading: cppreference: Move constructors.

Practice questions

What does `std::move` actually do?

Answer

It performs a cast: it converts its argument to an rvalue reference so overload resolution selects move operations. The actual transfer of resources happens in the move constructor or move assignment operator.

Why should move constructors be marked `noexcept`?

Answer

Containers such as std::vector need strong exception guarantees when they reallocate. If your move constructor might throw, they copy elements instead of moving them, losing the performance benefit.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?