Rule of Five
If a class defines any one of its five special member functions, it should explicitly define all five to avoid resource management bugs.
Rule of Fivesince C++11If a class explicitly defines any one of its destructor, copy constructor, copy assignment operator, move constructor, or move assignment operator, it should explicitly define all five.
Overview
The Rule of Five extends the pre-C++11 Rule of Three to account for the two move operations introduced in C++11. The Rule of Three held that any class requiring a user-defined destructor β typically because it directly manages a resource β almost certainly also needs a user-defined copy constructor and copy assignment operator, since the compiler-generated defaults perform a shallow memberwise copy that will alias the resource across objects.
C++11 raises the count to five by adding the move constructor and move assignment operator. More critically, C++11 introduced asymmetric generation rules that make the five functions interdependent:
- Declaring a destructor, copy constructor, or copy assignment operator suppresses compiler generation of move operations.
- Declaring a move constructor or move assignment operator causes the compiler to delete the copy operations.
A class that manages a raw resource and declares only a destructor will have no move operations β objects of that type copy everywhere, even in return statements and std::vector reallocations, silently degrading performance. These interactions make it unsafe to define some members and trust the compiler defaults for others. The Rule of Five makes that interaction explicit.
Compiler Generation Summary
| User declares | Move ctor generated | Move assign generated | Copy ctor generated | Copy assign generated |
|---|---|---|---|---|
| Nothing | Yes | Yes | Yes | Yes |
| Destructor only | No | No | Yes (deprecated) | Yes (deprecated) |
| Copy constructor | No | No | β | Yes (deprecated) |
| Copy assignment | No | No | Yes (deprecated) | β |
| Move constructor | No | No | Deleted | Deleted |
| Move assignment | No | No | Deleted | Deleted |
"Deprecated" means the compiler generates the operation but the C++11 standard deprecates the behaviour when the other copy/destroy special member is user-declared. "Deleted" means the compiler provides a deleted declaration, turning copy attempts into compile errors.
Syntax
A class managing a heap buffer with all five explicitly defined:
#include <cstring>
#include <cstddef>
#include <utility>
class Buffer {
public:
explicit Buffer(std::size_t n)
: data_(new std::byte[n]), size_(n) {}
~Buffer() { delete[] data_; } // destructor
Buffer(const Buffer& other) // copy constructor
: data_(new std::byte[other.size_]), size_(other.size_) {
std::memcpy(data_, other.data_, size_);
}
Buffer& operator=(const Buffer& other) { // copy assignment
if (this != &other) {
Buffer tmp(other);
swap(tmp);
}
return *this;
}
Buffer(Buffer&& other) noexcept // move constructor (C++11)
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
Buffer& operator=(Buffer&& other) noexcept { // move assignment (C++11)
if (this != &other) {
delete[] data_;
data_ = std::exchange(other.data_, nullptr); // std::exchange: C++14
size_ = std::exchange(other.size_, 0);
}
return *this;
}
private:
void swap(Buffer& other) noexcept {
std::swap(data_, other.data_);
std::swap(size_, other.size_);
}
std::byte* data_; // std::byte: C++17
std::size_t size_;
};Examples
Copy-and-Swap for Strong Exception Safety
The copy-and-swap idiom implements copy assignment in terms of the copy constructor, providing the strong exception guarantee at minimal extra cost:
Buffer& Buffer::operator=(const Buffer& other) {
Buffer tmp(other); // throws here? 'this' is unchanged
swap(tmp); // noexcept; old data released when tmp is destroyed
return *this;
}A unified assignment operator accepting by value handles both copy and move assignment through overload resolution:
// C++11: 'other' is copy-constructed from lvalues, move-constructed from rvalues
Buffer& Buffer::operator=(Buffer other) noexcept {
swap(other);
return *this;
}Defaulting All Five to Document Intent
When members are RAII types that manage their own resources, the compiler-generated five are correct. Explicitly = default-ing them makes intent visible and prevents silent suppression if the class later gains a base with deleted operations:
class Widget {
public:
Widget() = default;
~Widget() = default;
Widget(const Widget&) = default;
Widget& operator=(const Widget&) = default;
Widget(Widget&&) noexcept = default; // C++11
Widget& operator=(Widget&&) noexcept = default; // C++11
private:
std::string name_;
std::vector<int> data_;
};This is the Rule of Zero: delegate resource ownership entirely to standard RAII types, let the compiler generate all five, and declare them = default explicitly to lock in the intent.
Move-Only Types
Some handles cannot be meaningfully copied. Delete the copy operations and provide only move:
class UniqueFile {
public:
explicit UniqueFile(int fd) noexcept : fd_(fd) {}
~UniqueFile() {
if (fd_ >= 0) ::close(fd_);
}
UniqueFile(const UniqueFile&) = delete;
UniqueFile& operator=(const UniqueFile&) = delete;
UniqueFile(UniqueFile&& other) noexcept // C++11
: fd_(std::exchange(other.fd_, -1)) {}
UniqueFile& operator=(UniqueFile&& other) noexcept { // C++11
if (this != &other) {
if (fd_ >= 0) ::close(fd_);
fd_ = std::exchange(other.fd_, -1);
}
return *this;
}
private:
int fd_;
};Explicitly deleted copy operations are clearer than relying on the implicit deletion that move-constructor declaration would cause β the intent is visible without knowing the generation rules.
Best Practices
Mark move operations noexcept. std::vector reallocation uses move only when the move constructor is noexcept; otherwise it falls back to copy. A missing noexcept silently reduces push_back to O(n) copy work. If your move implementation can genuinely throw, that is a design problem to fix, not a reason to omit noexcept.
Prefer the Rule of Zero. Wrapping raw resources in std::unique_ptr, std::shared_ptr, or similar RAII handles before storing them as members eliminates the need for any of the five. The standard containers, smart pointers, and lock guards exist precisely so application classes need not implement resource management themselves.
Check for self-assignment in direct copy assignment. Copy-and-swap handles this automatically. A direct implementation that frees the old resource before copying from other will corrupt data if this == &other. The guard if (this != &other) is the minimum requirement.
Do not add noexcept to copy constructors. Copy constructors commonly allocate memory, which can throw std::bad_alloc. Marking them noexcept converts allocation failure into std::terminate, which is almost never the intended behaviour.
Common Pitfalls
Defining only the destructor and getting no moves. The most common violation: a class owns a raw pointer, declares a destructor to delete it, and gains neither move constructor nor move assignment. Every return, push_back, and emplace_back produces a copy. Performance regresses silently; no compiler warning is required.
Shallow copy from compiler-generated copy constructor. A class with a raw owning pointer that relies on the compiler default copy will have two objects sharing the same allocation. The first destructor to run leaves the second with a dangling pointer. The compiler does not diagnose this.
Moved-from object left in an invalid state. A moved-from object is still subject to destruction. If the move constructor transfers a pointer without nulling the source, both objects will delete[] the same memory. The moved-from pointer must be set to nullptr (or -1 for file descriptors, etc.) so the destructor becomes a no-op.
Forgetting noexcept on the move constructor when using std::vector. The vector's strong exception guarantee requires that, during reallocation, either all elements move successfully or none do. It can only guarantee this if moves are noexcept; otherwise it copies. Profile before assuming your container is moving elements.
See Also
reference/idioms/value-semanticsβ value types and the semantics these five functions collectively definereference/language/abstract-classesβ polymorphic base classes require a virtual destructor, which in turn suppresses compiler-generated movesreference/language/const-correctnessβ copy constructor and copy assignment parameters must beconst T&