Copying and Moving Objects
When an object is copied, assigned or moved, C++ calls special member functions. The compiler writes them for you, and for most classes they're exactly right. This lesson shows what they are, when you must write your own, and the rules of zero, three and five that keep you out of trouble.
The special member functions
| Function | Runs when |
|---|---|
Copy constructor T(const T&) | A new object is made as a copy: T b = a; |
Copy assignment T& operator=(const T&) | An existing object is overwritten: b = a; |
Move constructor T(T&&) | A new object takes over a temporary or std::moved object |
Move assignment T& operator=(T&&) | An existing object takes over another's contents |
Destructor ~T() | The object's lifetime ends |
The compiler-generated versions copy, move or destroy each member in turn. Because std::string, std::vector and smart pointers already do the right thing, a class built from them needs no special functions of its own — the rule of zero.
Watching them run
#include <iostream> #include <string> #include <utility> #include <vector> class Widget { public: explicit Widget(std::string name) : name_(std::move(name)) {} Widget(const Widget& other) : name_(other.name_) { std::cout << "copy " << name_ << '\n'; } Widget(Widget&& other) noexcept : name_(std::move(other.name_)) { std::cout << "move " << name_ << '\n'; } Widget& operator=(const Widget& other) { name_ = other.name_; std::cout << "copy-assign " << name_ << '\n'; return *this; } Widget& operator=(Widget&& other) noexcept { name_ = std::move(other.name_); std::cout << "move-assign " << name_ << '\n'; return *this; } private: std::string name_; }; Widget make() { return Widget{"made"}; } int main() { Widget a{"a"}; Widget b = a; // copy Widget c = std::move(a); // move b = c; // copy-assign b = make(); // move-assign from a temporary (make() itself copies nothing) std::vector<Widget> v; v.reserve(2); v.push_back(std::move(b)); // move into the vector }
copy a move a copy-assign a move-assign made move made
Note that make() printed nothing: returning a temporary constructs it directly in place (guaranteed copy elision). Mark move operations noexcept — containers like std::vector only use them during reallocation if they can't throw.
When you must write them: owning a raw resource
A class that directly owns a raw resource gets the wrong defaults. Copying a raw pointer copies the address, not the data — two objects then "own" the same memory and both delete it. Here's a minimal buffer class done properly, following the rule of five: if you write any of destructor, copy or move operations, write (or delete) all of them:
#include <algorithm> #include <cstddef> #include <iostream> #include <utility> class Buffer { public: explicit Buffer(std::size_t size) : size_(size), data_(new int[size]{}) {} ~Buffer() { delete[] data_; } Buffer(const Buffer& other) : size_(other.size_), data_(new int[other.size_]) { // deep copy std::copy(other.data_, other.data_ + size_, data_); } Buffer& operator=(Buffer other) noexcept { // copy-and-swap: handles copy and move assignment swap(*this, other); return *this; } Buffer(Buffer&& other) noexcept // steal the resource : size_(std::exchange(other.size_, 0)), data_(std::exchange(other.data_, nullptr)) {} friend void swap(Buffer& a, Buffer& b) noexcept { std::swap(a.size_, b.size_); std::swap(a.data_, b.data_); } int& operator[](std::size_t i) { return data_[i]; } std::size_t size() const { return size_; } private: std::size_t size_; int* data_; }; int main() { Buffer a(3); a[0] = 42; Buffer b = a; // independent copy b[0] = 7; Buffer c = std::move(a); // a is now empty std::cout << b[0] << ' ' << c[0] << ' ' << a.size() << ' ' << c.size() << '\n'; b = c; std::cout << b[0] << '\n'; }
7 42 0 3 42
That's a lot of code for something std::vector<int> already does — which is exactly the point. Write the rule of five only for classes whose job is managing a resource; everything else should hold such classes as members and follow the rule of zero.
Disabling copies
Some objects shouldn't be copied: a file handle, a mutex, a unique network connection. Delete the copy operations. Such types can often still be moved:
#include <string> class Connection { public: explicit Connection(std::string host) : host_(std::move(host)) {} Connection(const Connection&) = delete; Connection& operator=(const Connection&) = delete; Connection(Connection&&) = default; Connection& operator=(Connection&&) = default; private: std::string host_; }; int main() { Connection a{"db.example.com"}; Connection b = std::move(a); // fine: moving Connection c = b; // error: copying is deleted }
main.cpp:17:16: error: call to deleted constructor of 'Connection'
Summary
- Rule of zero: build classes from members that manage themselves, and write none of the special functions. This is the default.
- Rule of five: if a class manages a raw resource and needs a custom destructor, it needs custom (or deleted) copy and move operations too.
- Use
= defaultto ask for the compiler's version explicitly, and= deleteto forbid an operation. - Mark moves
noexcept. A moved-from object must still be destructible and assignable.
Try it yourself
This class follows the rule of zero. Copy and move some objects and confirm everything behaves without any special functions written.
Road trip (2 songs) Road trip (3 songs)