Add a destructor to a class. You have just changed how that class copies, how it moves, and whether it moves at all, without touching any of those operations. The special member functions in C++ are not independent declarations you opt into one at a time; they are generated by rules that read each other, and the rules cascade in both directions.

The idea

The rules of three, five, and zero look like a progression of advice, but they are all consequences of one fact: the compiler generates the operations you did not write, and it stops generating them the moment you write any of their siblings. The C++ Core Guidelines state both directions. Declaring any copy, move, or destructor function, even as =default or =delete, will suppress the implicit declaration of a move constructor and move assignment operator. Declaring a move constructor or move assignment operator, even as =default or =delete, will cause an implicitly generated copy constructor or copy assignment operator to be defined as deleted. The non-obvious consequence is that the failure mode is silent. As the guideline puts it, unwanted effects include turning all potential moves into more expensive copies, or making a class move-only. Neither produces a diagnostic.

Rule of three

cppreference’s statement is short: if a class requires a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, it almost certainly requires all three.

The argument for it is about what the compiler will do in your absence. Because C++ copies and copy-assigns objects of user-defined types in various situations, such as passing and returning by value and manipulating a container, these special member functions will be called if accessible, and if they are not user-defined they are implicitly defined by the compiler.

The specific case where the implicit versions are wrong: cppreference says they should not be used if the class manages a resource whose handle is an object of non-class type, such as a raw pointer or a POSIX file descriptor, whose destructor does nothing and whose copy constructor and assignment operator perform a shallow copy, meaning they copy the value of the handle without duplicating the underlying resource.

That is the whole bug in one sentence. Two objects holding the same char*, each with a destructor that runs delete[], is a double free. Needing a destructor is the signal that the handle is not self-managing, and a non-self-managing handle needs the copy operations written too.

cppreference’s rule_of_three class is the minimal demonstration: a private char* cstring, a constructor that does cstring = new char[std::strlen(s) + 1]; then std::strcpy(cstring, s);, a destructor doing delete[] cstring;, a copy constructor delegating to the string constructor with other.cstring, and a copy assignment implemented through copy-and-swap. The example’s main builds o1{"abc"}, copy-constructs o2{o1}, builds o3("def"), then copy-assigns o3 = o2, printing abc abc def abc.

Two details in that class are easy to miss. The copy assignment carries the comment that copy-and-swap prevents potential storage reuse, which is an honest note that the idiom trades an allocation for brevity and exception safety. And the copy constructor delegating to the string constructor is why the allocation logic appears only once.

The same rule covers the opposite intent. cppreference notes that classes managing non-copyable resources through copyable handles may have to define copy assignment and copy constructor as = delete (since C++11), and calls this another application of the rule of three, because deleting one and leaving the other to be implicitly defined is typically incorrect. A file handle that must not be duplicated is still a matched set; the matched value is just “deleted” instead of “written.”

Rule of five

The move operations were added in C++11 and were fitted into the existing generation rules, which is where the trap comes from. cppreference states it precisely: because the presence of a user-defined (including = default or = delete declared) destructor, copy constructor, or copy assignment operator prevents implicit definition of the move constructor and the move assignment operator, any class for which move semantics are desirable has to declare all five special member functions.

The exact suppression conditions are on the move constructor page: an implicit move constructor is declared only if no user-defined move constructors are provided and there are no user-declared copy constructors, no user-declared copy assignment operators, no user-declared move assignment operators, and no user-declared destructor.

So a class that correctly followed the rule of three in 2009 became a class that never moves in 2011. cppreference’s assessment of that outcome is the calibrating sentence: unlike the rule of three, failing to provide a move constructor and move assignment is usually not an error, but a missed optimization opportunity. Correctness survives, performance quietly does not. The mechanism is in Move Semantics and Rvalue References, where cppreference’s own example prints move failed! for the class that added a destructor.

The rule_of_five class shows what the two extra members look like when written by hand. The move constructor is rule_of_five(rule_of_five&& other) noexcept : cstring(std::exchange(other.cstring, nullptr)) {}, and the move assignment is noexcept, swapping cstring with other.cstring and returning *this. std::exchange sets the source’s pointer to null in the same expression that reads it, which is what keeps the moved-from object safely destructible. Both are marked noexcept, which matters because containers consult that when deciding whether they may move elements during reallocation.

Rule of zero

The target is to write none of them. cppreference states the rule as a division of labor: classes that have custom destructors, copy/move constructors, or copy/move assignment operators should deal exclusively with ownership, which follows from the Single Responsibility Principle, and other classes should not have custom destructors, copy/move constructors, or copy/move assignment operators.

The example is the shortest class on the page. rule_of_zero holds a std::string cppstring; and a constructor taking a const std::string&. No destructor, no copy operations, no move operations, and every one of them is correct, because the member already has correct ones and the compiler-generated versions do the right thing member by member.

The C++ Core Guidelines carry the same rule as C.20, which cppreference cites directly, phrased as: if you can avoid defining default operations, do. The reason given is that it is the simplest and gives the cleanest semantics. Its Named_map example holds a string name and a map<int, int> rep, declares only an explicit constructor, and the guideline’s comment on Named_map nm2 {nm}; is that since std::map and string have all the special functions, no further work is needed.

The practical translation for a class that does hold a raw resource: do not write five functions, write one member. Replace the char* with a std::string, the T* with a `std::unique_ptr<T>`, the file descriptor with a small wrapper type that owns exactly that one thing. Then the enclosing class is back to zero. The guideline’s own enforcement note points at the same refactor from the other end: a class with a pointer-and-size pair of members and a destructor that deletes the pointer could probably be converted to a vector.

This is RAII applied recursively. Every resource gets exactly one owning wrapper, and every class above that layer composes wrappers instead of managing resources.

Polymorphic bases cannot reach zero

cppreference notes that when a base class is intended for polymorphic use, its destructor may have to be declared public and virtual, and that this blocks implicit moves and deprecates implicit copies, so the special member functions have to be defined as = default. Its base_of_five_defaults class does exactly that: four defaulted copy and move operations plus virtual ~base_of_five_defaults() = default;. But it then warns that this makes the class prone to slicing, which is why polymorphic classes often define copy as = delete, pointing at guideline C.67. The Core Guidelines’ CloneableBase shows the resulting shape: a virtual unique_ptr<CloneableBase> clone() const;, a defaulted virtual destructor and default constructor, and all four copy and move operations = delete. The guideline adds that defining only the move operations or only the copy operations would have the same effect, but stating the intent explicitly for each special member makes it more obvious to the reader.

The generic form, and why it is stated as all-or-none

cppreference says the slicing problem leads to a generic wording of the rule of five, which is Core Guideline C.21: if you define or =delete any copy, move, or destructor function, define or =delete them all.

C.21’s reasoning starts from semantics rather than mechanics. The semantics of copy, move, and destruction are closely related, so if one needs to be declared, the odds are that others need consideration too. Its bad example is struct M2 with a pair<int, int>* rep member, a destructor doing delete[] rep;, and no copy or move operations, followed by x = y; using the default assignment. The verdict: given that special attention was needed for the destructor, the likelihood that the implicitly defined copy and move assignment operators will be correct is low, and here we would get double deletion.

Three more notes from C.21 are worth carrying around.

Intent should be explicit. If you want a default implementation while defining another, write =default to show you are doing so intentionally, and if you do not want a generated default function, suppress it with =delete.

Relying on an implicitly generated copy operation in a class with a destructor is deprecated. The language itself has taken a position on the rule of three.

And the argument types are easy to get wrong. The guideline lists the canonical five for a class X: virtual ~X() = default;, X(const X&) = default;, X& operator=(const X&) = default;, X(X&&) noexcept = default;, and X& operator=(X&&) noexcept = default;. It then observes that a minor mistake, such as a misspelling, leaving out a const, using & instead of &&, or leaving out a special function, can lead to errors or warnings, and concludes with the sentence that makes zero the goal rather than five: to avoid the tedium and the possibility of errors, try to follow the rule of zero.

C.21’s enforcement is stated as a mechanical check, which is a good self-review rule: a class should have a declaration, even a =delete one, for either all or none of the copy, move, and destructor functions.

The decision in one pass

Does this class own a raw resource? If no, write zero special member functions. If yes, first try to stop owning it directly by wrapping the resource in a dedicated one-resource type or an existing standard library type, and then write zero. If the class genuinely must be the owner (usually because you are the one writing the wrapper), write all five, or write the destructor plus four = deletes if the resource cannot be duplicated. Never write one or two.

Sources

  • “The rule of three/five/zero,” cppreference.com. https://en.cppreference.com/w/cpp/language/rule_of_three.html . Supports the rule of three statement and its rationale, the shallow-copy problem with non-class handles, the rule_of_three class and its abc abc def abc output, deleting copy operations as another application of the rule, the rule of five with the suppression clause covering = default and = delete declarations, the rule_of_five members including std::exchange and noexcept, the missed-optimization framing, the rule of zero statement and rule_of_zero class, the citation of Core Guideline C.20, and the polymorphic-base discussion including base_of_five_defaults, slicing, and the generic C.21 wording.
  • “C++ Core Guidelines,” isocpp.github.io. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines . Supports C.20 with the Named_map example and the vector-refactor enforcement note, and C.21 with the bidirectional suppression rules, the moves-become-copies and move-only warnings, the M2 double-deletion example, the =default/=delete intent note, the deprecation of implicit copy in a class with a destructor, the canonical five signatures for X and the misspelling warning, the closing advice to follow the rule of zero, the CloneableBase slicing-suppression example, and the all-or-none enforcement rule.
  • “Move constructors,” cppreference.com. https://en.cppreference.com/w/cpp/language/move_constructor.html . Supports the exact conditions under which an implicit move constructor is declared: no user-defined move constructors, no user-declared copy constructors, no user-declared copy assignment operators, no user-declared move assignment operators, and no user-declared destructor.