C++ classes either manage a resource or they do not, and that single distinction decides how many special member functions a class has to write. The Rule of Zero says write none when possible. The Rule of Three is the older C++98 advice for resource-owning classes. The Rule of Five is the modern version that adds move operations. This chapter covers choosing which rule applies before writing code, and the discipline that keeps a class consistent once it is chosen.
Every C++ class has five special member functions: destructor, copy constructor, copy assignment, move constructor, move assignment. The three rules below tell which of those five to write by hand.
| Rule | Era | Applies When | What You Write |
|---|---|---|---|
| Rule of Zero | C++11 onward | The class does not directly own any resource | Nothing. Compiler-generated specials are correct |
| Rule of Three | C++98 | The class directly owns a resource through a raw handle | Destructor, copy constructor, copy assignment |
| Rule of Five | C++11 onward | The class directly owns a resource and you care about move performance | All five specials |
There is also an informal "Rule of Zero/Five" that is a discipline rule: write either zero of the five or all five. Writing some but not others is where bugs come from. The compiler will delete or fail to generate the ones not written, and the class ends up with a mismatched set that works in one place and breaks in another.
Picking the wrong rule is not slow. It is wrong. A Rule of Three class compiled today loses moves without warning and gets copies inserted wherever the code thought it had moves. Both compile, both run, one of them is 100x slower under std::vector growth.
Before deciding which rule applies, answer one question. Does this class hold something that has to be released exactly once when the object dies? A new'd allocation, a file handle, a socket, a database connection, an acquired mutex, an OS resource that needs an explicit free call. If yes, the class manages a resource directly and owes it special members. If no, the compiler's generated versions are correct and should be left alone.
The trap: "manages a resource" sounds like it applies to almost anything. It does not. Holding a std::string member is not managing a resource directly. The string manages its own buffer; the class holds the string. Same for std::vector, std::unique_ptr, std::shared_ptr, std::ifstream, and any other type that already does RAII. The class delegates resource ownership to the member, and the member's special functions do the right thing when the class is copied, moved, or destroyed.
The diagram tells the whole story. Most classes written today should land on Rule of Zero. The only ones that need Rule of Five are the small number that wrap a raw resource directly. Rule of Three is mostly a historical artifact: writing new C++ in Rule of Three style almost certainly should be Rule of Five instead, because the two move operations are cheap to add and pay for themselves the first time the class lands in a std::vector.
Rule of Zero is the goal. A class structured so it never directly owns a resource has less code, the compiler generates correct specials for it, and there is no single line where a memory bug can be introduced.
A class that follows Rule of Zero by design. It models an order in an online store. It has a customer, a list of item IDs, and a status. None of those are raw resources, so the class has no special functions of its own.
Note what is not there. No destructor. No copy constructor. No copy assignment. No move constructor. No move assignment. The compiler synthesized all five, and they all do the right thing because the members are well-behaved RAII types. Copying an Order deep-copies both the string and the vector. Moving an Order transfers their internal buffers without allocating. Destroying an Order destroys both members, which release their buffers.
A move of Order is six pointer assignments (three for the string, three for the vector) and runs in nanoseconds. A copy allocates and memcpy's both buffers. The compiler-generated move was free to write, and it works without explicit code.
The discipline that makes Rule of Zero work is choosing the right members. Replace any raw pointer with a smart pointer. Replace any C-string buffer with std::string. Replace any dynamically-sized array with std::vector or std::array. Replace any file handle with std::fstream or a custom RAII wrapper. Once the members do RAII themselves, the enclosing class falls into Rule of Zero by accident, which is the clearest way to land there.
Rule of Five applies when a class wraps a resource directly. This means it holds something the standard library does not already wrap. A raw pointer to a heap allocation, a file descriptor from open(), a window handle from a graphics library, a mutex from a C API. These are the cases that require writing the destructor by hand, and once the destructor is written, the other four must be written too.
The next example is a CartBuffer class that holds a heap-allocated array of items. This is a deliberately raw-pointer example to show what Rule of Five looks like. In real code, no one would write this, because std::vector<int> already exists and does the same job correctly. This example exists to make the rule visible.
This is Rule of Five at work, using the copy-and-swap idiom for the assignment operator. Copy-and-swap is covered in its own section. For now, the destructor frees the buffer exactly once, the copy constructor allocates a fresh buffer and copies the contents, and the move constructor steals the pointer and leaves the source empty. The class manages a raw resource, so all of these have to be hand-written.
Compare against the same class implemented with Rule of Zero:
The second version is six lines instead of forty. It does the same thing. It cannot leak. It cannot double-free. It cannot get the move-vs-copy-assignment subtlety wrong because there is no subtlety to get wrong. This is why Rule of Zero is the preferred default, and Rule of Five is the fallback for the small number of classes that wrap a raw resource.
The Rule of Five lesson has the full table of when each special function is generated, deleted, or suppressed. The short version, framed as a best practice: declaring any one of the five interacts with the other four in ways that are easy to forget.
The interactions that bite most often:
The defensive practice is to use = default and = delete explicitly whenever any of the five has been touched. If a destructor was written and copies and moves are still wanted, write them out as = default. If a move constructor was written and the class should remain non-copyable, write the copy operations as = delete. Do not rely on the compiler's implicit behavior, because the rules are subtle enough that the next person reading the code (which might be the author in six months) will get them wrong.
Adding the virtual destructor would have killed the implicit moves without warning. Writing = default for all five makes the intent visible and prevents the regression. The class is still Rule of Zero in spirit (none of the bodies are hand-written), but the declarations are explicit. Some style guides call this "Rule of Zero with explicit defaults" and it is a useful middle ground when one of the five has to be touched for a reason that is not resource management, like adding a virtual destructor for a base class.
The five special functions, with the relationships that make them a single unit:
The arrows are not a call graph. They are a coherence relationship. The destructor decides what releasing the resource means. The copies decide what duplicating it means. The moves decide what transferring it means. If those three definitions do not agree, the class behaves correctly in some situations and corrupts memory in others.
There is a classic pattern for writing copy assignment that avoids two of the difficult bugs: self-assignment failure and exception unsafety. It is called copy-and-swap, and it appears in textbook examples often enough to be worth understanding even when mostly writing Rule of Zero classes.
The idea: instead of writing copy assignment to manipulate the current object's state directly, let the parameter be passed by value (which makes a copy automatically), then swap the parameter with *this, then let the parameter go out of scope (which destroys what used to be in *this). Three steps. No explicit delete. No explicit new. No self-assignment problem.
Three things make this idiom valuable. First, self-assignment is automatically safe. If a caller writes handle = handle, the parameter is constructed as a separate copy of the same object, then swapped in. Nothing is ever freed before being read. Second, the assignment is automatically exception-safe. If the copy constructor (which runs when the parameter is constructed) throws, *this is never touched, so the original object remains intact. Third, one function handles both copy assignment and move assignment. If the caller passes an lvalue, the parameter is copy-constructed. If they pass an rvalue (via std::move), the parameter is move-constructed. Either way, the assignment body is the same swap.
The cost of copy-and-swap is one extra construction in the copy case. Some performance-critical classes avoid it for that reason and write the two assignment operators separately. For most classes, the safety and clarity win.
Copy-and-swap performs one extra deep copy on copy assignment compared to a hand-written version that reuses the destination's buffer. For most classes this does not matter. For classes where assignment happens millions of times per second on large buffers, write the two assignment operators by hand instead.
noexcept on Move Operations: Why It Is Not OptionalThe best-practice angle around noexcept on move operations is the focus here, because forgetting noexcept on a move constructor is the most common silent-performance bug in real C++ codebases.
The headline rule: standard library containers use the move constructor during reallocation only if it is marked noexcept. If a move can throw, std::vector::push_back falls back to copy when it grows, and the performance benefit of moves is lost. The class still compiles. The container still works. It just runs at copy speed.
The vector reallocated and used the copy constructor for both existing elements, despite the move constructor being available. Adding noexcept to the move constructor's signature changes the output to two MOVE lines instead. Same allocations, same memcpy patterns, except now the move path runs instead of the copy path.
The rule is unambiguous. If a move constructor cannot throw, mark it noexcept. Almost every well-written move constructor satisfies this, because the body typically copies a few pointers and nulls out the source. Pointer assignment does not throw. Integer assignment does not throw. The compiler does not infer noexcept from "this body does not throw," though. It has to be written on the declaration. Marking move operations noexcept is part of the contract offered to the rest of the codebase.
A std::vector<CartBuffer> of 10,000 entries growing from capacity 8192 to 16384 copies every element (10,000 fresh allocations and memcpy's) instead of moving them if the move ctor is not noexcept. For buffers of 1 MB each, that is 10 GB of unnecessary heap traffic on a single resize. The fix is one keyword.
The other side of the noexcept contract: do not lie. If a move constructor can throw (which is unusual but possible if it allocates), do not mark it noexcept. Marking a throwing function noexcept causes std::terminate to be called if an exception escapes, which crashes the program. The compiler cannot catch this.
Three production bugs come up repeatedly in Rule of Five classes. Knowing the patterns by name allows spotting them in code review without having to derive them from first principles.
The shallow copy bug. A class declares a destructor that calls delete on a raw pointer member, but the copy constructor is the compiler-generated one (which copies the pointer, not what it points at). Both copies hold the same pointer. Whichever destructor runs second tries to delete already-freed memory, producing a double-free.
What is wrong with this code?
When b is destroyed, it deletes the array. When a is destroyed, it deletes the same array again. The second delete[] either crashes immediately or corrupts the heap. On glibc, the error is double free detected in tcache 2. The class has a destructor that owns a resource but no matching copy constructor, which is the Rule of Three violation.
Fix: Either add a deep-copy constructor (and the rest of the five), or replace the raw pointer with a std::unique_ptr<int[]> and fall back to Rule of Zero. The smart-pointer version is almost always the better answer.
The self-assignment bug. A class writes copy assignment that frees its current resource before reading from the source. If a caller writes x = x, the source and destination are the same object. Freeing destroys both, then reading from the source reads from freed memory.
What is wrong with this code?
If &other == this, the delete[] invalidates other.data_ because it is the same pointer as this->data_. The subsequent memcpy reads freed memory, which is undefined behavior.
Fix: Use copy-and-swap (the parameter is a separate object, no self-assignment issue), or check if (this == &other) return *this; at the top of a hand-written version, or allocate the new buffer before deleting the old one.
The silent move-loss bug. A class declares a destructor (or any of the five), which suppresses the implicit move operations. Every place that used to move now copies. The class compiles, the tests pass, and a profiler reports a year later that 30% of CPU is in copy constructors that should have been moves.
The fix is the rule-of-zero-or-five discipline. If one of the five is declared, declare all five. Use = default for the ones the compiler should generate. Do not let the implicit suppression remain.
The previous chapter covered RAII as the foundation of resource management in C++. The Rule of Five is what makes a custom RAII class correct. Writing a class whose destructor releases a resource signs the class up for the Rule of Five. The destructor is the easy part. The copy, move, and assignment operations are where most RAII implementations get subtle bugs.
A useful framing: every RAII class is a Rule of Five class. The reason most code does not feel like Rule of Five is that it uses existing RAII classes (std::vector, std::string, std::unique_ptr) instead of writing new ones. Those types absorbed the Rule of Five. Code stays Rule of Zero because it delegated to them.
When a new RAII class is needed (wrapping a C handle, a third-party resource, an OS object), the Rule of Five is non-negotiable. std::unique_ptr and std::shared_ptr cover 95% of cases. For the remaining 5%, the rule applies.
9 quizzes