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.
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) noexcepttakes an rvalue reference, transfers the resource and leaves the source valid but empty. std::move(x)doesn't move anything; it only castsxto 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. noexcepton moves matters:std::vectoronly 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
#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
noexcepton 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::movemoves. 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::moveis 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.