Back then, I had written a small comment on Scott’s post that deserves some further elaboration. In this post I am going to wrap up my thoughts about the two posts and propose a “Rule of All or Nothing”.
The Rule of Zero
In his blog post, Martinho discusses the implications of move operations for the old C++98/03 Rule of Three, extending it to the Rule of Five, which essentially means that if a class defines one of the Big Five, i.e. copy and move constructors, assignment operators and destructor, it should probably define all of them.
He further states that the Big Five are mostly needed to implement the handling of ownership and that this should be handled by special classes like smart pointers. If the handling of ownership is put away into smart pointers, the classes that contain those smart pointers don’t need to have manually implemented move and assignment operations or destructors, because the compiler will generate them with the correct semantics, if possible and needed. The Rule of Zero reads as follows:
Rule of Zero by Martinho Fernandes: Classes that have custom destructors, copy/move constructors or copy/move assignment operators should deal exclusively with ownership. Other classes should not have custom destructors, copy/move constructors or copy/move assignment operators.
The Rule of the Five Defaults
The Rule of the Five Defaults, as proposed by Scott Meyers, has essentially the same spirit as the Rule of Zero. The difference is that instead of not having declared the Big Five for ordinary classes they should be all explicitly defaulted. That means they still should be generated by the compiler, but explicitly instead of implicitly.
His rationale was that if you add one of them, for example a destructor for a debugging session, the compiler won’t generate the move operations any more and fall back to copy operations. By explicitly defaulting them it is ensured that such subtle changes won’t happen. With his changes the rule would look like this:
Rule of the Five Defaults: Classes that declare custom destructors, copy/move constructors or copy/move assignment operators should deal exclusively with ownership. All other classes should explicitly default their destructors, copy/move constructors and copy/move assignment operators.
I have a few concerns about the Rule of Zero, at least its wording, and about the Rule of the Five Defaults. I too think that a garden variety class should not implement any of the Big Five manually, but I think the Rule of Zero, as stated above, does not take into account some cases, and the Rule of the Five Defaults is overly broad.
It’s not only about ownership…
In a previous article about exception handling, I have cited Jon Kalb: RAII should mean Responsibility Acquisition Is Initialization. So RAII classes don’t only handle ownership of resources but any kind of responsibility. So we should change that word in the Rule of Zero. And while we are at responsibilities, the Single Responsibility Principle demands that such a class never deals with multiple responsibilities.
… and not only about responsibilities, either
You probably know the rule that base class destructors should be declared either public and virtual, or protected and nonvirtual. If not, here is a link to Herb Sutters old GotW site. In both cases you have to define them, although you can and should default them. When dealing with base classes, it can be undesirable to have copy and move assignment public, so you should check on those, too, i.e. the Rule of Five applies.
What does “custom” mean?
I am not 100% sure about what is meant by “custom” Big Five in the Rule of Zero. It might either mean manually implemented or not implicitly generated. That means, it is unclear to me whether virtual or nonpublic, but defaulted Big Five are included in that rule. I could not find a clear source that solves the issue, so maybe a bit of clarification is needed for others, too.
As Scott stated in his post, classes always have a destructor, and the rule should be about declaring the Big Five. Like the example with the base classes shows, it does not matter whether they are manually implemented or defaulted, so the word “custom” should be left out completely, it only matters if one of the Big Five is declared or not.
The Rule of the Five Defaults would demand to default the Big Five in every normal class. This would mean not only writing those five lines on each and every class, it would also mean having to read them, or, most probably, to ignore them every time a maintainer looks a the header. When the majority of the classes have those five defaulted lines, you quickly learn to read past them, and you will miss crucial information, e.g. when one of them is deleted instead of defaulted, because that information just drowns in the noise.
For that reason, I don’t agree with the Rule of the Five Defaults. In my comment on Scott’s blog post I proposed a compromise, the Rule of All or Nothing: By default, don’t provide any of the Big Five for a class. But whenever you write one of them, explicitly default all the others.
That way, not only the problem with the non-generated move operations disappears, but it also gives a huge benefit to the communication between the author and any maintainer: Having one of the Big Five declared now clearly sends a message “look, this is not one of your boring normal classes, something is happening here”. Having the other Big Five explicitly defaulted means “… and yes, I have thought about the Rule of Five, too”.
The Rule of All or Nothing
Wrapping it all together here is a wording for the Rule of All or Nothing:
Rule of All or Nothing: A class that needs to declare one or more of the destructor, copy/move constructor or copy/move assignment operations should explicitly default the rest of those operations. All other classes should not declare any destructor, copy/move constructor or copy/move assignment operator.
At first, I had an additional small sentence in there: Such a class can be either a base class or a RAII class that acquires a single responsibility. I believe this to be not true, there could be other cases where declaring one of the Big Five might be necessary.
One example is the debug-info destructor in Scott’s post, although I would consider that destructor a temporary one and it should be removed with the other defaulted Big Five after the debugging has ended. Another example would be – uhm – Singletons. Yeah, I know, they are very contronversial, but some people still use them.
What do you think? Did I miss a point somewhere?
Update: here’s the corresponding section of the core guidelines: http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-zero