Jonathan Boccara's blog

The Rule of Zero in C++

Published April 23, 2019 - 0 Comments

Daily C++

rule of zero

 

Now that we’re clear on the Compiler-generated Functions, the Rule of Three and the Rule of Five, let’s put this to use to reflect on how to use the “= default” feature to have expressive and correct code.

Indeed, C++11 added the possibility to require from the compiler that it write a default implementation for these methods of a class:

class X
{
public:
   X() = default;
   
   X(X const& other) = default;
   X& operator=(X const& other) = default;
   
   X(X&& other) = default;
   X& operator=(X&& other) = default;
   
   ~X() = default;
};

But the compiler can also generate those functions even if we don’t specify them in the interface. We saw that this C++ feature had some intricacies, but in the above case anyway, the code is perfectly equivalent to this:

class X
{

};

This rises a question: if the compiler is able to provide a default implementation, should we write = default to be more explicit even when that doesn’t change the generated code? Or is it gratuitous verbosity? Which way is more expressive?

We had the debate with my colleagues (hat tip to them), I dug around to realize that it was a hot debate: the C++ Core Guidelines have an opinion, Scott Meyers has an opinion, and they are not really agreeing with each other. Let’s see what this is all about.

The C++ Core Guidelines & R. Martinho Fernandes: The Rule of Zero

The C++ Core Guidelines are very clear about this question, with the opening guideline on constructors stating:

C.20: If you can avoid defining default operations, do.

Right. Pretty clear. Now what’s the rationale behind this guideline?

Reason It’s the simplest and gives the cleanest semantics. [If all members] have all the special functions, no further work is needed.

And the guideline goes on by saying that this is known as the “Rule of Zero“.

This term was coined by R. Martinho Fernandes, in a 2012 blog post (thanks Lopo and Reddit user sphere991 for digging up the post).

What’s the Rule of Zero exactly? It goes like this: Classes that declare custom destructors, copy/move constructors or copy/move assignment operators should deal exclusively with ownership. Other classes should not declare custom destructors, copy/move constructors or copy/move assignment operators (Rule of Zero slightly rephrased by Scott Meyers).

According to the Rule of Zero, there are two options regarding the functions that the compiler can generate: either they all have a non-trivial implementation that deals with ownership, or none of them is declared.

Except that if you look at it closely, the Rule of Zero doesn’t say anything about the default constructor X(). It only mentions the 5 functions that otherwise participate to the Rule of Five. As a reminder, the Rule of Five says that if one of the 5 resource-management functions (copy/move constructors, copy/move assignment operators, destructor) had a non-trivial implementation, the others should certainly have a non-trivial implementation too.

So what about the default constructor? If its implementation is trivial, should we declare it with = default or not declare it at all and let the compiler do the job?

But C++ Core Guideline C.20 seems to encourage us not to declare it either:

C.20: If you can avoid defining default operations, do.

Still pretty clear.

Scott Meyers: The Rule of the Five Defaults

Scott Meyers writes in response to the Rule of Zero that it presents a risk.

Indeed, declaring any one of the 5 functions has a side effect on the automatic generation of the move operations. A pretty harsh side effect, because it deactivates the automatic generation of the move operations. (If you’re wondering why the move operations specifically, have a look at the refresher on Compiler-generated Functions, the Rule of Three and the Rule of Five).

In particular, if you add a destructor to the class:

class X
{
public:
   ~X() { /* log something in the dtor */ }
};

Then it loses its move operations. BUT it doesn’t lose its copy operations! So client code will continue to compile, but will silently call copy instead of move. This is not good.

In fact, if you declare the destructor explicitly, even if you use the default-generated implementation:

class X
{
public:
   ~X() = default;
};

Then the class loses its move operations!

Defending the Rule of Zero

One argument of the supporters of Rule of Zero to answer Scott’s concern is: why would we implement just a destructor for a class in the first place? To this, Scott brings up the use case of debugging. For example, it can be useful to put a breakpoint or a trace in the destructor of a class to follow along at runtime what is going on in a challenging program.

Another argument of the proponents of the Rule of Zero against Scott’s concern is that the compiler is able to catch the risky situation with a warning anyway. Indeed, with the flag -Wdeprecateed, clang outputs the following warning for the above class X:

warning: definition of implicit copy constructor for 'X' is deprecated because it has a user-declared destructor [-Wdeprecated]

And when we try to invoke a move operation on that class that silently implements copy:

X x1;
X x2 = std::move(x1);

We also get a warning:

note: implicit copy constructor for 'X' first required here

This is nice but it’s just a warning, it’s not standard, and only clang emits it as far as I know. The standard merely mentions that “in a future revision of this International Standard, these implicit definitions could become deleted”. There has been a proposal for the standard to make this behaviour officially illegal, but it hasn’t been accepted.

The Rule of the Five Defaults

Instead, Scott Meyers argues in favour of another rule, the Rule of the Five Defaults: always declare the 5 resource-management functions. And if they are trivial, use = default:

class X
{
public:
   X(X const& other) = default;
   X& operator=(X const& other) = default;
   
   X(X&& other) = default;
   X& operator=(X&& other) = default;
   
   ~X() = default;
};

Note that like in the C++ Core Guidelines, the poor default constructor X() has been left out of the discussion.

However, if we follow the Rule of the Five Defaults, there is not much choice left for the default constructor. Indeed, if there is at least one other declared constructor, the compiler doesn’t generate the default constructor automatically. And here we don’t have one, but two declared constructors: the copy constructor and the move constructor.

So with the Rule of the Five Defaults, if we want a trivial default constructor then we need to declare it:

class X
{
public:
   X() = default;

   X(X const& other) = default;
   X& operator=(X const& other) = default;
   
   X(X&& other) = default;
   X& operator=(X&& other) = default;
   
   ~X() = default;
};

So maybe we should call that the Rule of the Six Defaults. Anyway.

Good interfaces for good programmers

I don’t think the debate has been won over by any of the parties at this point.

Applying the Rules of the Five (or Six) defaults produces more code for each interface. In the case of very simple interfaces, such as a struct that bundles a couple of objects together, that can double or triple the size of the interface, and express not that much.

Should we produce all this code to make the interface explicit?

To me, this comes down to the question of what programmers will think the class does by looking at its interface.

If you know the rules of C++, you’ll know that a class that doesn’t declare any of the 6 methods expresses that it has them all. And if it declares all of them except move operations, then it’s probably a class coming from C++98 and therefore it doesn’t comply with move semantics (which is by the way another argument in favour of the Rule of Zero: who knows what the future will be? Maybe in C++29 there will be a &&& constructor, and the rule of zero will express that the class wants defaults for everything, including &&&).

The risk is that someone designed a class without knowing what they were doing, or that a reader of the code doesn’t know enough C++ to infer what a class could do. And I don’t think we should burden the code with a safety net of 5 = defaulted functions for each and every type of the codebase.

Instead, we should assume that

  • fellow developers know what they’re doing and care about the messages expressed (or implied) by their interfaces,
  • fellow developers know enough C++ to read what an interface expresses (or implies).

Maybe you’re thinking “oh, I know a junior guy that completely proves those assumptions wrong”. And indeed, we all have to start as a beginner. But the thing is, we need to strive to make those assumptions the reality.

This is the point of code reviews, trainings, dailies, mentoring, pair programming, books, and so on. This is an investment but I think we need to level up with the code, and not the other way round.

I know it’s a controversial question, and I’d love to hear your opinion on it. Do you think we should write code as if everyone on the project was up to speed on the rules of C++?

To conclude I’ll leave the closing word to Arne Mertz, that summarized the debate with a rule that everyone agrees on, the “Rule of All or Nothing”:

As long as you can, stick to the Rule of Zero, but if you have to write at least one of the Big Five, default the rest.

Now let’s take a break and go get a refreshing drink with zero calories. I mean water, of course.

You may also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin