Rule of zero, three and five

C++ · Advanced · 14 min

Getting the special members wrong causes double frees, leaks and silent slow copies; these rules make it systematic.

The compiler can generate five special members besides the default constructor: the destructor, copy constructor, copy assignment, move constructor and move assignment. Which ones it generates depends on what you declare yourself.

Rule of three/five. If a class needs a custom destructor, it almost certainly owns a resource directly, so the generated copy operations (which copy the raw pointer) would be wrong. Define or delete all five together. Declaring any of the destructor or copy operations suppresses the implicit move operations, so a class that only adds a destructor quietly becomes copy-only.

Rule of zero. Better: don't own resources directly. Make every member manage itself, such as std::string, std::vector, std::unique_ptr or a small RAII wrapper, and declare none of the five. The generated versions then do the right thing member by member, and the class is copyable or move-only exactly when its members are. Most classes in good C++ code follow the rule of zero.

Write the full five only for a class whose job is to manage one resource, like a buffer or a handle wrapper. Tools that help:

  • = default asks for the generated version explicitly (and keeps it trivial when possible).
  • = delete forbids an operation: T(const T&) = delete; makes a type non-copyable.
  • Copy-and-swap: take the assignment parameter by value and swap with it. One operator covers copy and move assignment and gives the strong exception guarantee.
  • std::exchange(o.p_, nullptr) reads the source pointer and nulls it in one step in move constructors.

A polymorphic base declares virtual ~Base() = default;, which suppresses moves, so default them explicitly if the hierarchy needs to be movable.

Example

#include <cstring>
#include <memory>
#include <string>
#include <utility>
#include <vector>

// Rule of zero: members own the resources; the compiler writes all five correctly.
class Document {
    std::string title_;
    std::vector<std::string> lines_;
    std::unique_ptr<int[]> index_;      // makes Document move-only automatically
public:
    explicit Document(std::string t) : title_{std::move(t)} {}
};

// Rule of five: only for a class whose job IS owning one resource.
class CBuffer {
    char* p_;
    std::size_t n_;
public:
    explicit CBuffer(std::size_t n) : p_{new char[n]{}}, n_{n} {}
    ~CBuffer() { delete[] p_; }
    CBuffer(const CBuffer& o) : p_{new char[o.n_]}, n_{o.n_} { std::memcpy(p_, o.p_, n_); }
    CBuffer(CBuffer&& o) noexcept : p_{std::exchange(o.p_, nullptr)}, n_{std::exchange(o.n_, 0)} {}
    CBuffer& operator=(CBuffer o) noexcept {   // copy-and-swap: covers copy AND move
        std::swap(p_, o.p_); std::swap(n_, o.n_); return *this;
    }
};

int main() {
    Document d{"notes"};
    Document d2 = std::move(d);         // OK; `Document d3 = d2;` would not compile
    CBuffer a{16}, b{a};                // deep copy
    b = std::move(a);
}

Remember

  • Rule of zero: compose RAII members and declare no special members
  • Rule of five: a custom destructor means you must handle copy and move too
  • Declaring a destructor or copy operation suppresses implicit moves

Watch out for

  • Adding an empty ~T() {} "for debugging" and silently turning every move of T into a copy.

Check yourself

  1. A class holds `std::vector<int>` and `std::unique_ptr<Foo>` and declares no special members. What can you do with it?
  2. A class declares only a destructor `~T();`. What happens to `T b = std::move(a);`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: en.cppreference.com/w/cpp/language/rule_of_three