The rule of zero and the rule of five are two answers to the same question: what do you have to write when a class owns a resource? The rule of zero says write nothing at all, because nothing is needed if you do not manage memory yourself. The rule of five says if you must manage it yourself, then think carefully about all five special members. Modern C++ makes the first answer easy enough that the second is rarely needed.
The rule of zero
The rule of zero states that a class should not define any of the destructor, the copy constructor, the copy assignment operator, the move constructor or the move assignment operator. If the class holds its data in members that manage themselves, the compiler-generated versions are exactly right.
#include
#include
#include
class Fleet {
public:
Fleet(const std::string& name, const std::vector& mileages)
: name_(name), mileages_(mileages) {}
int total_mileage() const {
int total = 0;
for (int m : mileages_) {
total += m;
}
return total;
}
private:
std::string name_;
std::vector mileages_;
};
int main() {
Fleet original("north", {1000, 2000});
Fleet copied = original; // no special members needed
Fleet assigned("south", {50});
assigned = original;
std::cout << original.total_mileage() << ' '
<< copied.total_mileage() << ' '
<< assigned.total_mileage() << '\n';
return 0;
}The output shows all three objects with the same total:
3000 3000 3000Nothing was written by hand, and copying is correct, because the std::string and the std::vector know how to copy themselves.
The rule of five
The rule of five applies when a class directly manages a resource — usually memory obtained with new or a file handle obtained from the C library.
If you write any one of these five, think about all five.
- Destructor — releases the resource.
- Copy constructor — creates a new owner with its own copy of the resource.
- Copy assignment operator — releases the old resource and takes a copy of the new one.
- Move constructor — takes over the resource instead of copying it.
- Move assignment operator — releases the old resource and takes over the new one.
Here is a complete implementation that follows the rule.
#include
#include
#include
class RawBuffer {
public:
explicit RawBuffer(int bytes) : size_(bytes), data_(new char[bytes]) {}
~RawBuffer() {
delete[] data_;
}
RawBuffer(const RawBuffer& other)
: size_(other.size_), data_(new char[other.size_]) {
for (int i = 0; i < size_; ++i) {
data_[i] = other.data_[i];
}
}
RawBuffer& operator=(const RawBuffer& other) {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = new char[other.size_];
for (int i = 0; i < size_; ++i) {
data_[i] = other.data_[i];
}
}
return *this;
}
RawBuffer(RawBuffer&& other) noexcept
: size_(other.size_), data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr;
}
RawBuffer& operator=(RawBuffer&& other) noexcept {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = other.data_;
other.size_ = 0;
other.data_ = nullptr;
}
return *this;
}
int size() const { return size_; }
private:
int size_;
char* data_;
};
int main() {
RawBuffer a(16);
RawBuffer b = a;
RawBuffer c(8);
c = a;
std::cout << a.size() << ' ' << b.size() << ' ' << c.size() << '\n';
return 0;
}Every operation is well defined: the copies own separate memory, the moves take over the pointer and leave the source empty, and the destructors each free exactly what they allocated.
The rule of five in practice
The rule of five is a great deal of code to write for something the standard library already does. The pragmatic path is to make the raw pointer disappear.
#include
#include
#include
#include
#include
class Buffer {
public:
explicit Buffer(int bytes) : data_(static_cast(bytes)) {}
int size() const { return static_cast(data_.size()); }
private:
std::vector data_;
};
int main() {
Buffer buffer(1024);
Buffer copied = buffer;
Buffer moved = std::move(buffer);
std::cout << copied.size() << ' ' << moved.size() << '\n';
return 0;
}The output shows both objects with their buffers intact:
1024 1024Because the only member is a std::vector, the class defines none of the five special members and the rule of zero applies. The vector handles copying, moving and cleanup.
Which rule should you follow?
Use the rule of zero whenever you can. It means fewer lines, fewer opportunities for a double free, and simpler code for the next person to read.
Use the rule of five when you genuinely must own a raw resource. Two situations justify it: you are wrapping a C library handle that must be released with a C function, or you are writing a low-level container whose whole purpose is to manage memory directly.
In between sits a third option that is worth knowing. You can keep the rule of five for the public interface but implement it in terms of a smart pointer, which does the copying and deleting for you.
#include
#include
class FileHandle {
public:
explicit FileHandle(std::FILE* file) : file_(file) {}
~FileHandle() {
if (file_ != nullptr) {
std::fclose(file_);
}
}
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
FileHandle(FileHandle&& other) noexcept : file_(other.file_) {
other.file_ = nullptr;
}
FileHandle& operator=(FileHandle&& other) noexcept {
if (this != &other) {
if (file_ != nullptr) {
std::fclose(file_);
}
file_ = other.file_;
other.file_ = nullptr;
}
return *this;
}
std::FILE* get() const { return file_; }
private:
std::FILE* file_;
};
int main() {
FileHandle handle(std::fopen("notes.txt", "w"));
if (handle.get() != nullptr) {
std::fputs("hello", handle.get());
}
return 0;
}Deleting the copy operations with = delete is the right move for a resource that genuinely cannot be duplicated. The class then has a move constructor, a move assignment operator and a destructor, and the compiler will refuse to let you copy it.
Quick reference
- The rule of zero: define none of the five special members and let the members manage themselves.
- The rule of five: if you define one, think about all five.
- The five are the destructor, copy constructor, copy assignment, move constructor and move assignment.
- Deleting a special member with
= deleteturns a copy error into a compile error. std::unique_ptrandstd::vectormake the rule of zero easy to follow.- Wrapping a C handle usually means a move-only class with a destructor.
Try it yourself: write a class that owns a raw buffer with a destructor only, and copy it. Run the result under the address sanitizer and read the double-free report. Then replace the raw pointer with a [[std::vector]] and confirm the copy works. Finally, add a move constructor and watch the copy from a temporary become a move instead.