The Rules of Three, Five and Zero
This post introduces the Rules of Three, Five and Zero and explain which one you should be using and when. A follow-on post will dive a bit deeper into implementing the Rule of Five for different cases.
Now, C++ has long been famed for its principle of RAII (Resource Acquisition Is Initialization). The term relates to the ability to manage resources, such as memory, through the five special member functions: the copy and move constructors, destructors and assignment operators. Often, when RAII is mentioned it is in reference to destructors being deterministically invoked at the end of a scope. A little ironic, given the already awkward name. But the rest of RAII’s superpowers are equally important. While many languages just distinguish between “value types” and “reference types” (e.g. C# defines value types in structs and reference types in classes), C++ gives us a much richer canvas for dealing with identity and resources through this set of special member functions.
But even before C++11, this flexibility came at a cost in terms of complexity. Some of the interactions are subtle and easy to get wrong. So back as far as 1991, Marshall Cline coined “The Rule of Three”, a simple rule of thumb that covered most cases. When C++11 introduced move semantics this was upgraded to “The Rule of Five”. Then R. Martinho Fernandes coined “The Rule of Zero” suggesting that it trumps The Rule of Five as a default. But what are all these rules? And do we have to follow them?
The Rule of Three becomes The Rule of Five
The Rule of Three suggests that if you need to define any of a copy constructor, copy assignment operator or destructor then you would usually need to define “all three”. I put “all three” in quotes, there, because that advice is outdated as of C++11. Now, with move semantics, there are two additional special member functions: the move constructor and move assignment operator. So the Rule of Five is just an expansion that suggests that if you need to define any of the five, then you probably need to define or delete (or at least consider) all five.
(This statement is not as strong as in the Rule of Three because if you do not define move operations then they will not be generated – and calls will fall back to copy operations. This would not be incorrect – but perhaps a missed opportunity to optimize.)
Unless you are compiling for strictly earlier than C++11, you should be following the Rule of Five.
Either way this makes sense. If you need to define a custom special member function (other than a default constructor) then it is usually because you are managing some resource. In that case, you will need to consider what happens to it at each stage of its lifetime. Note that there are various reasons that default implementations of special member functions may be suppressed or deleted, which we will look at more in the second article.
Here’s an example, loosely inspired by
indirect_value from P1950:
Notice that we used the copy-and-swap (and move-and-swap) idiom(s) to implement the assignment operators to prevent leaks and automatically handle self-assignment (we could also combine the two operators into one that takes its argument by value, but I wanted to show both functions in this example).
Now both rules start with, “if you need to define any of …”. Sometimes the negative space is interesting. The implicit side to these rules is that there are useful cases where you do not need to define any of the special member functions and things will work as expected. It turns out that this may be the most important case, but to see why, we need a little reframing. Enter the Rule of Zero.
The Rule of Zero
If no special member functions are user-defined then (subject to member variables) the compiler provides default implementations for all of them. The Rule of Zero is simply that you should prefer the case where no special member functions need to be defined. This divides into two cases:
- Your class defines a pure value type and any state it has consists of pure value types (e.g. primitives).
- Any resources maintained as part of your class’ state are managed by classes that are specialized for resource management (e.g. smart pointers, file objects, etc).
The second case deserves a little more explanation. Another formulation is that any given class should directly manage, at most, one resource. So if you have memory to manage then you should use or write a class specialized for managing that memory – whether that is a smart pointer, an array-based container, or something else. These resource managing types would follow the Rule of Five. But such classes should be quite rare – the standard library covers most common cases with its containers, smart pointers and stream objects. A class that uses a resource managing type should “just work” by following the Rule of Zero.
Maintaining this strict distinction keeps your code simpler, cleaner, and more focused – and easier to write correctly. “No code has less bugs than no code”, so needing to write less code (especially resource management code) is usually a good thing.
So, again, the Rule of Zero makes sense – and, indeed, the Sonar analysers will guide you to this with S493 - The “Rule-of-Zero” should be followed.
When to use which rule?
In a way, the Rule of Zero encompasses the Rule of Five, so you should just follow it. But another way to think of it is to follow the Rule of Zero, by default. Fall back to the Rule of Five when you find you need to write any specialized resource owning classes (which should be rare). Again, this is captured by S3624 - When the “Rule-of-Zero” is not applicable, the “Rule-of-Five” should be followed.
The Rule of Three only comes into it if you are working strictly with pre-C++11.
But does this really cover all cases?
When the Rules of Three, Five and Zero are not enough
Polymorphic base classes are a common case where the above rules apply, but seem a little heavyweight. Why? Because such classes should have a (defaulted) virtual destructor (S1235 - Polymorphic base class destructor should be either public virtual or protected non-virtual). That does not mean they should have any of the other special member functions – in fact, it is good practice for polymorphic base classes to be pure abstract base classes – with no functionality.
Providing public copy and move operations on polymorphic hierarchies makes them prone to slicing – where the difference between the static and dynamic types are lost in the copy. If copyability (or moveability) is required then they should be via virtual methods. A virtual
clone() method is common in this case. Implementations of these virtual methods may use the copy and move operations – in which case they can be implemented or defaulted as protected members – preventing accidental use from outside. Otherwise, which is most of the time, they should just be deleted.
Implementing or deleting all the special member functions can get a bit tedious, especially if you are working in a code base that has a lot of polymorphic base classes (although this is quite rare these days, at least in newer code). One way to work around this – in fact the only way prior to C++11 – is to privately inherit from a base class that has these five definitions (or, before C++11, make the “deleted” functions private and unimplemented). This is still a valid option and, arguably, brings us back to the Rule of Zero.
However, it turns out that deleting the move assignment operator is all we need to do. Because of how the interactions between special member functions are specified, this will have the same effect (and, in fact, maybe slightly better, as we’ll see in the next article).
If that seems strange or suspicious – or if you want to dig more into implementing the Rule of Five for different cases, read on to the second part of this series where we will dive deeper into all of this, as well as how those interactions are specified.