Jonathan Boccara's blog

Compiler-generated Functions, Rule of Three and Rule of Five

Published April 19, 2019 - 0 Comments

Daily C++

When you read a class interface that defines some basic functions (constructors, destructors, assignment) but not all of them, don’t you wonder what that code means, and what functions will be available for that class in practice? I often do.

To clarify this type of situation, I suggest we make a recap of what class functions the compiler generates in C++. Being clear on this will let us:

  • better understand such code,
  • reflect on higher-level questions, such as whether = default makes code more expressive or not, which we’ll explore in the next post.

I went to my compiler and tested out various combinations of user-defined and compiler-defined functions. You’ll find the results synthesized in this article, with some rationale that I took from Effective C++ (item 5 and 6) and Modern Effective C++ (item 17).

Hope you’ll find those results useful.

What functions the compiler can generate

The idea of compiler-generated functions is that, if some functions of a class are so trivial to write that their code would nearly be boilerplate, the compiler will take care of writing them for you.

This feature has been here since C++98, where the compiler would try to generate:

  • a default constructor X(), that calls the default constructor of each class member and base class,
  • a copy constructor X(X const& other), that calls a copy constructor on each member and base class,
  • a copy assignment operator X& operator=(X const& other), that calls a copy assignment operator on each class member and base class,
  • the destructor ~X(), that calls the destructor of each class member and base class. Note that this default-generated destructor is never virtual (unless it is for a class inheriting from one that has a virtual destructor).

With C++11, the compiler generates 2 new functions related to move semantics:

  • a move constructor X(X&& other), that calls a move constructor of each class member and base class,
  • a move assignment operator X& operator=(X&& other), that calls a move assignment operator on each class member and base class.

Note that other functions have been proposed for automatic generation such as the comparison operators, and something related to this should hit C++20 with the spaceship operator. More on that later.

The Rule of Three and the Rule of Five

It is important to note that the default constructor has different semantics from the rest of the above functions. Indeed, all the other functions deal with the management of the resources inside of the class: how to copy them, how to dispose of them.

If a class holds a handle to a resource such as a database connection or an owning raw pointer (which would be the case in a smart pointer for example), those functions need to pay special care to handle the life cycle of that resource.

The default constructor only initializes the resource, and is closer in semantics to any other constructor that takes values, rather than to those special functions that handle resource life cycle.

Let’s now count the functions in the above bullet points that handle the resource management of the class:

  • there are 3 in C++98 (4 minus the default constructor),
  • there are 5 in C++11.

Which gives the “Rule of Three” in C++98, and the “Rule of Five” in C++11: let x be 3 in C++98 and 5 in C++11, then we have:

If you need to write the code for one of the x resource management functions, it suggests that your class’s resource handling is not trivial and you certainly need to write the other x – 1.  – Rule of x

When the compiler generates them

In some cases, the compiler won’t generate those functions.

If you write any of those functions yourself, the compiler won’t generate it. That’s pretty obvious.

If you don’t write one of the following (and you didn’t write move operations either, see below why):

  • a copy constructor,
  • a copy-assignement operator,
  • a destructor,

the compiler will try to generate them for you. Even if you’ve hand-written the other two. In some cases it may not succeed though, for instance if the class contains a const or reference member, the compiler won’t be able to come up with an operator=.

If you write any of the following:

  • a direct constructor X(int, double),
  • a copy constructor,
  • a move constructor,

then the compiler thinks: “the developer made the decision to write a constructor, maybe they don’t want a default one then”, and it doesn’t generate the default constructor. Which makes sense to me in the case of the value constructor, but that I find weird for the copy and move constructor, since like we said, default constructor and copy constructor have different semantics.

If you write any of the following:

  • a copy constructor,
  • a copy assignment operator,
  • a destructor,

the compiler thinks “there must be something complex about the resource management of that class if the developer took the time to write one of those”, and it doesn’t generate the move constructor nor the move assignment operator.

You may wonder, why does the compiler only refrain from generating the move functions and not the copy functions? After all, if it feels that the resource handling of the class is beyond its understanding, it shouldn’t generate any of the resource-handling functions, not even the destructor while we’re at it. That’s the rule of 5, isn’t it?

That’s true, and the reason for the observed behaviour is history. C++98 didn’t natively enforce the rule of 3. But C++11, that brought the move functions, also wanted to enforce the rule of 5. But to preserve backward compatibility, C++11 couldn’t remove the copy functions that existing code relied upon, only the move function that didn’t exist yet. This led to that compromise that we could (somewhat approximately) call the “rule of 2”.

Finally, if you write any of the following:

  • a move constructor,
  • a move assignment operator,

the compiler still thinks “there must be something complex about the resource management of that class if the developer took the time to write one of those”. But code that contains move operations can’t be pre-C++11. So there is no longer a backward compatibility and the compiler can fully enforce the rule of 5 by refraining from generating any of the 5 resource management functions.

= default and = delete

C++11 brought those two keywords that you can tack on the 6 functions that the compiler can generate.

If you write = default, as in:

class X
{
   X() = default;
};

Or in an implementation file:

X::X() = default;

Then you are explicitly asking the compiler to generate that function for you, and it will do it to the best of its abilities. It can fail though, if there is no possible default implementation. For a default constructor, that would be if one of the members of the class doesn’t itself have a default constructor for example.

And if you write = delete, you explicitly ask to remove that function, and the compiler can always satisfy this request. It looks like this:

class X
{
   X() = delete;
};

Or in an implementation file:

X::X() = delete;

The Rule of Zero

Now that we’re clear on what makes the compiler generate functions or not, we can move on to higher-level questions. In the next post, we’ll reflect over whether = default make an interface more expressive or not.

One of the aspects of that question will lead us the to Rule of Zero, which is to the Rule of Three and the Rule of Five what Batman Begins is to The Dark Knight and the The Dark Knight Rises, if I may say.

With that said, stay tuned for the next post.

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