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.
Rule of Zero: Delegate Resource Ownership
The most important corollary to the Rule of Five is knowing when not to apply it. If you replace every raw owning pointer with an RAII wrapper β std::unique_ptr for exclusive ownership, std::shared_ptr for shared ownership, std::vector/std::string for owned sequences β the compiler-generated special members are already correct. No destructor means no suppression, so all five compiler defaults are generated and do the right thing.
// Rule of Zero: no user-declared special members at all
class Document {
public:
explicit Document(std::string title, std::vector<std::string> paragraphs)
: title_(std::move(title)), paragraphs_(std::move(paragraphs)) {}
private:
std::string title_;
std::vector<std::string> paragraphs_;
};
// Compiler generates: destructor, copy ctor, copy assign, move ctor, move assign
// All are correct; Document is copyable and movable with no hand-written code.The same principle applies to unique resources: wrap them in std::unique_ptr rather than holding a raw pointer and writing a custom destructor.
// BEFORE: raw owning pointer β forces you into the Rule of Five
class Widget {
Resource* res_; // who deletes this? suppresses moves!
public:
~Widget() { delete res_; }
// ... must now write copy ctor, copy assign, move ctor, move assign
};
// AFTER: unique_ptr β Rule of Zero applies
class Widget {
std::unique_ptr<Resource> res_;
public:
// copy is deleted automatically (unique_ptr is move-only)
// move ctor and move assign are generated and correct
// destructor is generated and calls unique_ptr::~unique_ptr
};When you need copy semantics alongside unique ownership, implement the copy constructor manually (performing a deep copy) and = default the rest:
class Widget {
std::unique_ptr<Resource> res_;
public:
explicit Widget(std::unique_ptr<Resource> r) : res_(std::move(r)) {}
Widget(const Widget& other) // deep copy
: res_(other.res_ ? std::make_unique<Resource>(*other.res_) : nullptr) {}
Widget& operator=(const Widget& other) { // copy-and-swap
Widget tmp(other);
std::swap(res_, tmp.res_);
return *this;
}
Widget(Widget&&) noexcept = default; // C++11: pilfer the unique_ptr
Widget& operator=(Widget&&) noexcept = default;
~Widget() = default;
};The practical guideline: reach for std::unique_ptr first. Only drop to a raw owning pointer when you have a compelling reason (custom allocator, C interop), and at that point commit fully to the Rule of Five.
= default and = delete
C++11 introduced two keywords that give explicit control over special-member generation.
= default: Restoring Suppressed Members
= default asks the compiler to generate the canonical memberwise implementation, even for members that would otherwise be suppressed. It is useful in three situations:
Restoring a move constructor after declaring a copy constructor β declaring any copy or destructor suppresses the compiler from generating move operations. = default reinstates them:
class MyType {
public:
MyType(const MyType&); // user-defined: suppresses move ctor and move assign
MyType(MyType&&) = default; // C++11: restore suppressed move ctor
MyType& operator=(MyType&&) = default; // restore suppressed move assign
};Making the noexcept-ness explicit β an implicitly defaulted move constructor inherits noexcept from its members, but compiler-vendor differences mean this can silently become potentially-throwing (see the noexcept section below). Explicitly defaulted members let you annotate the guarantee:
class Container {
std::list<int> data_; // MSVC: std::list move ctor may throw
public:
Container(Container&&) noexcept = default; // commit to noexcept
Container& operator=(Container&&) = default;
};Documenting intent in a Rule of Zero class β even when the compiler would generate all five, writing = default makes the class's contract visible in the header and prevents silent breakage if a new data member suppresses a generation:
class Config {
public:
~Config() = default;
Config(const Config&) = default;
Config& operator=(const Config&) = default;
Config(Config&&) noexcept = default;
Config& operator=(Config&&) noexcept = default;
private:
std::string path_;
std::unordered_map<std::string, std::string> values_;
};Note: a "user-declared" member includes = default. Writing Foo(const Foo&) = default; still counts as a user declaration and therefore still suppresses the compiler from generating move operations β you must restore them explicitly.
= delete: Preventing Unwanted Operations
= delete marks a function as deliberately unavailable. Unlike the pre-C++11 idiom of declaring operations private without defining them, deleted functions are public, participate in overload resolution, and produce clear compile-time errors rather than opaque linker failures:
// Pre-C++11 (confusing linker errors, not ideal)
class OldNoncopyable {
OldNoncopyable(const OldNoncopyable&); // private, undefined
OldNoncopyable& operator=(const OldNoncopyable&);
};
// C++11 (clear, compile-time error, intent is obvious)
class Noncopyable {
public:
Noncopyable(const Noncopyable&) = delete;
Noncopyable& operator=(const Noncopyable&) = delete;
};Deleting the copy operations does not implicitly delete the move operations. The compiler will simply not declare them (because you declared a copy constructor). That means std::move(obj) quietly falls back to copy β which is deleted β producing a compile error. If you want a move-only type, write move operations explicitly and delete copies:
class MoveOnly {
public:
MoveOnly(MoveOnly&&) noexcept = default;
MoveOnly& operator=(MoveOnly&&) noexcept = default;
MoveOnly(const MoveOnly&) = delete;
MoveOnly& operator=(const MoveOnly&) = delete;
};Do not explicitly = delete move operations on a type that has copy operations. Deleted members still participate in overload resolution, so std::move(obj) would select the deleted move constructor and fail to compile β preventing use in generic code that attempts moves before falling back to copies:
// WRONG: breaks generic containers and algorithms
class BadCopyOnly {
public:
BadCopyOnly(const BadCopyOnly&) = default;
BadCopyOnly& operator=(const BadCopyOnly&) = default;
BadCopyOnly(BadCopyOnly&&) = delete; // forces compile error on std::move
BadCopyOnly& operator=(BadCopyOnly&&) = delete;
};
// CORRECT: omit move declarations; overload resolution falls back to copy
class CopyOnly {
public:
CopyOnly(const CopyOnly&) = default;
CopyOnly& operator=(const CopyOnly&) = default;
// move ops not mentioned: std::move(obj) invokes copy ctor
};noexcept on Move Operations and the Vector Pessimization
std::vector reallocation is the canonical place where noexcept on a move constructor has an observable performance impact.
Why std::vector Checks noexcept
When std::vector<T> exhausts its capacity and reallocates, it must transfer existing elements from the old buffer to the new one. The C++98 implementation always used copy construction, which preserves the strong exception guarantee: if any copy throws, the old buffer still holds all the original data, so push_back can roll back to its pre-call state with nothing lost.
C++11 introduced move semantics as a cheaper alternative. Moving elements instead of copying them is potentially much faster β pilfering the data from each element rather than allocating disjoint copies. But if the move constructor throws partway through the reallocation, the old buffer has been partially pilfered and cannot be restored. The strong guarantee is broken.
The standard library resolves this with std::is_nothrow_move_constructible_v<T>. During reallocation, the vector checks whether T's move constructor is noexcept:
- If
T(T&&)isnoexcept: use move construction β O(1) per element, amortized O(n) total. - If
T(T&&)is notnoexcept: fall back to copy construction β O(n) per reallocation, and each reallocation is O(n), making apush_backloop O(nΒ²) in the worst case.
Omitting noexcept on a move constructor silently regresses std::vector performance to C++98 copy semantics. This is called the vector pessimization.
#include <vector>
#include <string>
#include <utility>
struct SlowElement {
std::string data_;
// No noexcept! std::vector copies this on reallocation (C++98 behaviour).
SlowElement(SlowElement&& other)
: data_(std::move(other.data_)) {}
};
struct FastElement {
std::string data_;
// noexcept: std::vector moves this on reallocation (O(1) per element).
FastElement(FastElement&& other) noexcept
: data_(std::move(other.data_)) {}
};noexcept Propagation Through Members
noexcept-ness propagates through defaulted special members according to the noexcept-ness of each member's corresponding operation. A single non-noexcept member is enough to make the entire class non-noexcept-movable:
#include <list>
#include <string>
struct A { A(A&&); }; // no noexcept declared
struct B { B(B&&) noexcept; }; // explicitly noexcept
struct JustB { B b; }; // implicitly: JustB(JustB&&) is noexcept
struct AB { A a; B b; }; // implicitly: AB(AB&&) is NOT noexcept (A poisons it)
static_assert(!std::is_nothrow_move_constructible_v<A>);
static_assert( std::is_nothrow_move_constructible_v<B>);
static_assert( std::is_nothrow_move_constructible_v<JustB>);
static_assert(!std::is_nothrow_move_constructible_v<AB>);On MSVC, std::list<T>'s move constructor is not noexcept (it may allocate a sentinel node). Any class that holds a std::list member will silently acquire non-noexcept moves, triggering the vector pessimization for std::vector<ThatClass>. To fix this, explicitly default and annotate the move constructor:
class Document {
std::list<std::string> paragraphs_; // MSVC: std::list move may throw
public:
Document(Document&&) noexcept = default; // commit: bad_alloc β terminate
Document& operator=(Document&&) = default;
Document(const Document&) = default;
Document& operator=(const Document&) = default;
};
static_assert(std::is_nothrow_move_constructible_v<Document>);
// std::vector<Document> now uses moves, not copies, during reallocationThe noexcept here acts as a firewall: if the underlying move throws std::bad_alloc, the program calls std::terminate. For most applications this is the correct trade-off β std::bad_alloc is rarely recoverable anyway, and the performance of vector::push_back is a real concern.
Checking Your Types with static_assert
Add a static_assert to catch regressions early:
class MyResource {
public:
MyResource(MyResource&&) noexcept;
MyResource& operator=(MyResource&&) noexcept;
// ...
};
static_assert(std::is_nothrow_move_constructible_v<MyResource>,
"MyResource must be noexcept-movable for efficient vector storage");
static_assert(std::is_nothrow_move_assignable_v<MyResource>,
"MyResource must be noexcept-move-assignable");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&