Operator Overloading & Resource Management

The Rule of Three (and Five) & RAII

Why this matters

The last lesson fixed Buffer’s copy constructor, then its copy assignment operator, one at a time, in isolation. This lesson names the principle that made both fixes necessary at once: the Rule of Three. It’s not a new technique – it’s a statement about when the tools from the last lesson are needed, and it explains why they never show up alone.

The Rule of Three

If a class needs a custom destructor, it almost certainly needs a custom copy constructor and copy assignment operator too – and vice versa.

The reasoning connects directly to what you already know: a class needs a custom destructor specifically because it owns a resource that isn’t cleaned up automatically – Buffer’s delete[] data; exists because data is a raw pointer, and nothing frees it on its own. But that exact same fact – data is a raw pointer – is precisely why the default, compiler-generated copy constructor and copy assignment operator are wrong: they copy the pointer’s address, not what it points to, which is the shallow-copy bug from the last lesson. The same property of a class (owning a resource through a raw handle) breaks all three defaults simultaneously. If you find yourself writing a destructor, stop and ask whether you also need the other two – the answer is almost always yes.

The reverse holds too, and it’s just as important: Point2D, holding two plain doubles, needs none of the three. Its default destructor (do nothing, doubles clean themselves up) is correct; its default copy constructor and assignment (copy each double’s value) are correct. Writing your own versions for a class like Point2D would be redundant work reproducing exactly what the compiler already does. The rule isn’t “always write all three” – it’s “these three are a package deal: decide about all of them together, based on whether your class owns a resource a raw member can’t clean up on its own.”

RAII: tying a resource’s lifetime to an object’s lifetime

RAII (“Resource Acquisition Is Initialization”) is the name for the pattern all three lessons in this module have been building toward: acquire a resource in the constructor, release it in the destructor, and let the language’s own object-lifetime rules – automatic, deterministic, covered all the way back in Module 2’s stack rules and Module 4’s destructor timing – guarantee the release actually happens. Buffer is an RAII class: as long as every copy is a real, independent deep copy (which is exactly what the Rule of Three ensures), a Buffer object can never outlive its own array or leak it, no matter how it’s created, copied, or goes out of scope.

This matters beyond tidiness: a function can have many different exit points – an early return, several nested scopes ending at different points, in some languages an exception unwinding through multiple frames at once. Manually calling a cleanup function at every one of those points is easy to get wrong; a destructor runs automatically at all of them, because it’s tied to the object’s lifetime rather than to any specific line of code remembering to call it.

The Rule of Five: move operations join the package

C++11 added two more special member functions – the move constructor and move assignment operator – and extended the rule accordingly: if your class needs the original three, it can very likely also benefit from these two, for a reason the next lesson covers in full: copying Buffer always allocates and copies an entire array, even in situations where the original Buffer was about to be discarded anyway and its array could simply have been taken instead of duplicated. That’s what moving means, and it’s pure performance – correctness doesn’t require it (a class with only the Rule of Three is still completely correct, just sometimes slower than it needs to be).

A preview worth having in mind: the Rule of Zero

One more idea worth knowing exists, even before you have the tools to use it (Module 8 and Module 9 bring them): the best resource-owning classes often implement zero of these five functions themselves, by holding their resource through an existing type that already implements the Rule of Five correctly – a std::string, a std::vector, or a smart pointer, all covered later in this course – as a plain member, rather than a raw pointer. Copying, moving, and destroying the outer class then happens automatically, correctly, by copying, moving, and destroying that member. Buffer, written using a raw int *, is this module’s deliberate way of seeing why the rule exists at all, one manual step at a time – in your own code going forward, prefer reaching for an existing RAII-managed type over a raw owning pointer whenever one fits.

Try it
Output will appear here.

Exercises