The rule of zero and the rule of five
A class has five special member functions the compiler can write: destructor, copy constructor, copy assignment, move constructor and move assignment. Writing one of them by hand changes which of the others the compiler still generates, and that interaction is what the two rules are about.
The rule of zero says: own nothing directly, let members such as std::string and std::vector manage their resources, and write none of the five. The rule of five says: if you must own a resource, write all five consistently. Lessons 031 and 032 wrote them one at a time; this lesson puts the choice together.
The rule of zero
The class below has a std::string and a std::vector as members and no destructor, no copy and no move. The compiler generates all five, each one operating on the members, and the members already know how to copy, move and free their own memory.
The program copies a document, changes the copy and prints both. The two are independent, the copy is deep because std::string and std::vector copy deeply, and no line of the class mentions memory.
This is the shape most classes should have. Every resource is owned by a member that is itself a rule-of-zero class, and the outer class inherits correctness for free.
#include <iostream>
#include <string>
#include <vector>
class document
{
public:
document(std::string title, std::vector<int> pages) : title_(std::move(title)), pages_(std::move(pages)) {}
void add_page(int number)
{
pages_.push_back(number);
}
const std::string& title() const
{
return title_;
}
std::size_t page_count() const
{
return pages_.size();
}
private:
std::string title_;
std::vector<int> pages_;
};
int main()
{
document first{"guide", {1, 2}};
document second = first;
second.add_page(3);
std::cout << first.title() << ' ' << first.page_count() << '\n';
std::cout << second.title() << ' ' << second.page_count() << '\n';
}
g++ -std=c++23 -Wall -Wextra zero.cpp -o zero
./zero
guide 2
guide 3
The rule of five
A class that owns a raw array has to write all five. The destructor frees the array, the copy constructor and copy assignment duplicate it, and the move constructor and move assignment transfer the pointer and empty the source.
The program below declares all five in one class, with the smallest bodies that are correct. Two objects are created, one is copied and one is moved; the lines printed by each operation show which one ran.
The five are a unit: a class with a destructor and no copy duplicates the pointer, and a class with a copy and no move pays for a deep copy where a transfer was possible. Writing three and forgetting two is the failure this rule prevents.
#include <iostream>
#include <utility>
class buffer
{
public:
explicit buffer(int size) : size_(size), data_(new int[size]{})
{
std::cout << "construct\n";
}
buffer(const buffer& other) : size_(other.size_), data_(new int[other.size_])
{
for (int i = 0; i < size_; ++i) {
data_[i] = other.data_[i];
}
std::cout << "copy\n";
}
buffer& operator=(const buffer& other)
{
if (this != &other) {
buffer copy{other};
std::swap(size_, copy.size_);
std::swap(data_, copy.data_);
}
std::cout << "copy assign\n";
return *this;
}
buffer(buffer&& other) noexcept : size_(other.size_), data_(other.data_)
{
other.size_ = 0;
other.data_ = nullptr;
std::cout << "move\n";
}
buffer& operator=(buffer&& other) noexcept
{
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = other.data_;
other.size_ = 0;
other.data_ = nullptr;
}
std::cout << "move assign\n";
return *this;
}
~buffer()
{
delete[] data_;
}
int& at(int index)
{
return data_[index];
}
int size() const
{
return size_;
}
private:
int size_;
int* data_;
};
int main()
{
buffer a{2};
a.at(0) = 5;
buffer b = a;
buffer c = std::move(a);
std::cout << b.at(0) << ' ' << c.at(0) << ' ' << c.size() << '\n';
}
g++ -std=c++23 -Wall -Wextra five.cpp -o five
./five
construct
copy
move
5 5 2
A destructor costs the moves
Declaring a destructor stops the compiler from generating the move operations, because a class that needs clean-up usually needs to control how it is duplicated too. The class below has a destructor and no copy or move, so every insertion into the vector copies.
The program pushes two items into a vector that starts with one slot. The counter reports three copies and no moves, where the item of lesson 032 reported three moves.
The fix is to declare the move operations with = default, or to remove the destructor and let a member handle the resource. Deleting the copies and defaulting the moves is the usual form for a move-only type.
#include <iostream>
#include <vector>
int copies = 0;
class item
{
public:
item() = default;
item(const item&)
{
++copies;
}
~item() {}
};
int main()
{
std::vector<item> items;
items.reserve(1);
items.push_back(item{});
items.push_back(item{});
std::cout << "copies: " << copies << '\n';
}
g++ -std=c++23 -Wall -Wextra missing_move.cpp -o missing_move
./missing_move
copies: 3
= default and = delete
Writing = default asks the compiler for the member it would have generated, while keeping the declaration that changes what else is generated. Writing = delete removes the member and makes any use a compile error.
The program's token class deletes its copy operations and defaults its moves, which makes it a move-only type: a function can take it by value with std::move, and a copy is rejected by the compiler.
This is the modern way to write the buffer of lesson 031: let the compiler generate the move operations, delete the copies when sharing is wrong, and write a destructor only when a member cannot manage itself.
#include <iostream>
#include <utility>
class token
{
public:
explicit token(int value) : value_(value) {}
token(const token&) = delete;
token& operator=(const token&) = delete;
token(token&&) = default;
token& operator=(token&&) = default;
int value() const
{
return value_;
}
private:
int value_;
};
void consume(token t)
{
std::cout << "consumed " << t.value() << '\n';
}
int main()
{
token t{7};
consume(std::move(t));
}
g++ -std=c++23 -Wall -Wextra default_delete.cpp -o default_delete
./default_delete
consumed 7
A copy constructor hides the default one
Declaring any constructor, including a copy constructor, stops the compiler from generating the default constructor. The class below has a copy constructor and no default one, so box b; has no function to call.
The message says there is no matching function for the default construction and lists the candidate it found, which is the copy constructor. The fix is box() = default; or an explicit constructor that sets the members.
The same interaction exists for every special member: declare one and the compiler may stop generating another. That is why the rules of zero and five are stated in terms of the whole set rather than one function at a time.
class box
{
public:
box(const box&) = default;
private:
int value_ = 0;
};
int main()
{
box b;
}
g++ -std=c++23 -Wall -Wextra no_default.cpp -o no_default
no_default.cpp: In function 'int main()':
no_default.cpp:12:9: error: no matching function for call to 'box::box()'
12 | box b;
| ^
no_default.cpp:12:9: note: there is 1 candidate
no_default.cpp:4:5: note: candidate 1: 'constexpr box::box(const box&)'
4 | box(const box&) = default;
| ^~~
no_default.cpp:4:5: note: candidate expects 1 argument, 0 provided
no_default.cpp:12:9: warning: unused variable 'b' [-Wunused-variable]
12 | box b;
| ^
Exercises
- Add a move constructor to missing_move.cpp with = default and rerun. How many copies and moves does the counter report now?
- Write a move-only class that owns a std::vector and check that passing it by value with std::move compiles while a copy does not.
- Give document in zero.cpp an explicit destructor and see which operations stop being generated. Which line of the program breaks?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
Which five functions are the special members?
Answer
Destructor, copy constructor, copy assignment, move constructor, move assignment
The five are the operations the compiler can generate for an object's lifetime and copying.
-
What does writing a destructor do to the implicit moves?
Answer
It suppresses them, so a copy is used instead
The class now manages a resource, and the compiler will not guess the move.
-
What does the rule of zero recommend?
Answer
Own nothing directly and let members manage the resources
A class of members such as std::string and std::vector needs none of the five written by hand.
-
What does
= defaultask for?Answer
The compiler-generated version of a special member
The defaulted definition keeps the class's special member available without writing the body.
-
What does this program print?
#include <iostream> #include <string> #include <vector> struct guide { std::string title; std::vector<int> steps; }; int main() { guide first{"tour", {1, 2}}; guide second = first; second.steps.push_back(3); std::cout << first.title << ' ' << first.steps.size() << ' ' << second.steps.size(); }Answer
tour 2 3
The members manage themselves, so the copy has its own vector and the original keeps two steps.
-
What does this program print?
#include <iostream> #include <utility> class five { public: five() { std::cout << "construct\n"; } five(const five&) { std::cout << "copy\n"; } five(five&&) noexcept { std::cout << "move\n"; } ~five() = default; int a = 5, b = 5, c = 2; }; int main() { five first; five second = first; five third = std::move(second); std::cout << third.a << ' ' << third.b << ' ' << third.c << '\n'; }Answer
construct / copy / move / 5 5 2
Each special member prints its line in the order the objects are built, and the members keep their values.
-
What does the destructor-only class print instead of moves?
Answer
copies: 3
The declared destructor suppresses the implicit move constructor and assignment, so the vector copies.
-
What does this program print?
#include <iostream> #include <utility> class handle { public: handle() = default; handle(const handle&) = delete; handle(handle&& other) noexcept { value_ = other.value_; other.value_ = 0; } int value_ = 7; }; int main() { handle first; handle second = std::move(first); std::cout << "consumed " << second.value_; }Answer
consumed 7
The move is default-written by hand and the copy is deleted, and the moved object keeps the value.
-
What does a class with only a copy constructor report for
box b;?Answer
no matching function for call to 'box::box()'
Declaring any constructor removes the implicit default constructor, and the copy needs an argument.
-
What does
= deleteon a copy constructor mean?Answer
Copying is a compile error with a clear message
The deleted function is still found by overload resolution and reported as deleted.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev