The Rule of Zero revisited: The Rule of All or Nothing

In 2012, Martinho Fernandes coined the Rule of Zero in a blog post (which I no longer find online in 2024). In 2014, Scott Meyers wrote a blog post about a concern with that rule and proposed a Rule of Five Defaults.

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.

Discussion

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.

Boilerplate verbosity

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

Previous Post
Next Post
Posted in

9 Comments



  1. If you’re the sole developer in the project, then it’s easy to believe in and apply the rule of all or nothing. I’m saying “believe in” because one really has to believe in C++ in order to be in the mood of absorbing and remembering all those tiny rules that make a C++ codebase fast and robust.

    However, in a bigger team (>= 2), the rule of all or nothing means you’re relying on someone else to have the same level of knowledge/understanding (you may call that “devotion”) to keep the codebase tidy. With the rule of five defaults, at least those “who care more” have a chance to pave the road for others.

    Reply

    1. I think the rule is relatively easy, so even in bigger teams most team members should be able to remember it, and with some code review in place the rule is even more likely to be followed. Such rules are usually part of a coding style spec. For example, companies often include Scott Meyers’ Effective C++ items into such documents.

      However, I get your point that people are lazy and/or forgetting and tend to overlook such things. Therefore such rules are most useful if they can be checked by static analysis tools, which is the case for the rule of all or nothing.

      Reply



  2. “should explicitly default the rest of those operations” => “should explicitly declare the rest of those operations” … this would mean either delete’d or default’ed or defined. (maybe there’s a better term than declare here, but default is too restrictive.)

    Reply

    1. I don’t think it is too restrictive. The operations that you explicitly delete should be only the ones you have to delete, so they belong to the “needs to declare” part, not to “the rest”.

      If you don’t have to delete them, default them, even if they get implicitly deleted anyways.

      That way, the restrictive use of “default them” includes the rule to not alter compiler generated operations that don’t need to be altered.

      Reply

  3. Does “explicitly default” include declaring the functions as deleted?

    There are classes that are movable but not copyable, like unique_ptr and the standard stream classes. You don’t want to copy, because the objects are unique.

    We also have types where moving is the same as copying, and there is no real need to have both defaulted. But perhaps this is handled correctly by the Nothing case?

    Reply

    1. Deleting the functions is either included in the “needs to declare” part if the deletion has to be done explicitly or in the Nothing case if the class contains a member that prohibits the generation of those functions anyways. For example, a class that has a unique_ptr member is move-only by default, since the compiler can’t generate the canonical copy operations.

      Moving and copying is the same either when you implement both the same way, or when moving and copying is the same for all members and the compiler generated move/copy operations end up being the same. In that latter case the Nothing case applies unless you have to declare the destructor.

      In that case (destructor has to be declared, default move and copy are the same) you should default both move and copy operations: The alternatives would be either to not mention move operations at all or to explicitly delete them, which is essentially the same since the compiler does not generate moves when copy operations are declared. Except that no mentioning move would be bad communication, as said in the post.

      However, deleting the moves is not necessary and therefore at best gives no benefit. In the worst case, some maintainer adds a member that could benefit from move operations but gets always copied since move operations are deleted.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *