The Rule of Zero

In a previous article – ”The Rule of the Big Four (and a half)” we looked at resource management policies in C++.

Resource management is the general term for using the mechanisms in C++ to ensure that resources – files, dynamic memory, sockets, mutexes, etc – have their lifetimes automatically controlled so as to prevent resource leaks, deadlocks, etc. C++ refers to these mechanisms as RAII/RDID ( “Resource Acquisition Is Initialisation / Resource Destruction is Deletion”)

In this article we’ll have a look at a complementary guideline to help simplify your application code, without risking resource management issues – The Rule of Zero.

The term The Rule of Zero was coined by R. Martinho Fernandes in his 2012 paper (https://flamingdangerzone.com/cxx11/2012/08/15/rule-of-zero.html).  This article merely reflects Martinho Fernandes’ work and I highly recommend reading the original paper to get the full details of the concepts.

If you’re not already familiar with the concepts of resource management I’d suggest having a look at the previous articles – The Rule of Three (and a half) and The Rule of Four (and a half) before reading on.

The four categories of resource manager

From a resource management perspective we can categorise types in four ways:

  • Objects that can be both moved and copied
  • Objects for which it makes sense to copy but not move
  • Objects for which it makes sense to move but not copy
  • Objects which should neither be moved not copied.

The Rule of The Big four (and a half) is a guideline for implementing the copy/move policies for the types in your system. Essentially, it states:

  • If you have written a (non-default) destructor for a class you should implement the copy constructor and assignment operator for the class; or mark them as deleted.
  • Similarly, if you have written either a (non-default) copy constructor or assignment operator for the class you must write a destructor.
  • If your class can be moved you must implement both the move constructor and move assignment operator; or mark them as deleted.

With copy policy it is often performance issues – memory, speed, efficiency – that determine whether it is ‘sensible’ to copy a class. For example, it is possibly unwise to copy a 1MByte data file being owned as a resource!

In other cases the decision may be made based on whether replicating the owned resource may have detrimental consequences – for example, replicating an OS mutex could cause potential difficult-to-identify race conditions in code.

Moving a resource is commonly considered an efficiency optimisation for copying. However, the moved-from object must be left in an ‘empty’ state. What ‘defines’ empty (obviously) varies from object to object but a good rule-of-thumb is: If the class does not have a default constructor then it probably shouldn’t support move semantics.

image

image

The Rule of Zero

An alternative to “The Rule of The Big Four (and a half)” has appeared in the form of “The Rule of Zero”. “The Rule of Zero” basically states:

You should NEVER implement a destructor, copy constructor, move constructor or assignment operators in your code.

With the (very important) corollary to this:

You should NEVER use a raw pointer to manage a resource.

The aim of The Rule of Zero is to simplify your application code by deferring all resource management to Standard Library constructs, and letting them do all the hard work for you.

The Rule of Zero and dynamic memory

If your code must dynamically create objects prefer to use std::unique_ptr or std::shared_ptr. Use a std::unique_ptr if your class can be moved, but should not be copied:

image

Use a shared_ptr if you need to support copying as well as moving:

image

This code works because the compiler generates default implementations of the copy constructor, move constructor and assignment operators for your class. These default implementations will invoke the appropriate functions on the shared_ptr / unique_ptr (if available). The C++ Standard limits when these constructors are created, as follows:

Section 12.8/8:

If the definition of a class X does not explicitly declare a move constructor, one will be implicitly declared as defaulted if and only if

  • X does not have a user-declared copy constructor, and
  • X does not have a user-declared copy assignment operator,
  • X does not have a user-declared move assignment operator,
  • X does not have a user-declared destructor, and
  • The move constructor would not be implicitly defined as deleted.

(Section 12.8/22 specifies a very similar rule for assignment operators)

In other words, The Rule of Zero!

The Rule of Zero and strings

In the case of strings The Rule of Zero says prefer to use std::string over arrays of characters – particularly dynamically allocated (that is, variable-sized) arrays of characters.

image

Notice std::string supports both copying and move semantics; and after move leaves the string object as ‘empty’ (in this case, the null string).

The Rule of Zero and containers

The only built-in container in C++ is the array. This is sometime referred to as a ‘degenerate’ container because it is merely syntactic sugar coating pointer arithmetic.

Array notation basically hides the fact (the problem!) that arrays are little more than (raw) pointers. Because of this they are easily abused – either deliberately or accidently. Therefore, The Rule of Zero’s corollary still applies when it comes to arrays: don’t use them. This becomes particularly relevant when the arrays are created dynamically (with new).

Instead, The Rule of Zero prefers that we use Standard Library container classes.

image

If variable-sized containers aren’t required, prefer std::array to a built-in array. When a std::array is moved it calls the move assignment operator on each of its elements (as with all other objects, if the element in the array doesn’t support move it is copied instead)

image

Summary

The Rule of Zero is a guideline for simplifying application code, whilst avoiding the major problems associated with resource management in C++ programs.

The Rule of Zero is the ‘modern’ way to write C++ and there is very little good reason to fall back on C-style manual resource management. Experience and evidence has shown us that we, as developers, are just not particularly adept at it.

If you’d like to know more about topics like this have a look at our C++ training courses:

C++-501 – C++ for Embedded Developers

C++-502 – C++ for Real-Time Developers

AC++-501 – Advanced C++

AC++11-401 – Transitioning to C++11.

DP-403 – Design patterns for C++

Glennan Carnie
Dislike (0)
Website | + posts

Glennan is an embedded systems and software engineer with over 20 years experience, mostly in high-integrity systems for the defence and aerospace industry.

He specialises in C++, UML, software modelling, Systems Engineering and process development.

About Glennan Carnie

Glennan is an embedded systems and software engineer with over 20 years experience, mostly in high-integrity systems for the defence and aerospace industry. He specialises in C++, UML, software modelling, Systems Engineering and process development.
This entry was posted in C/C++ Programming, Design Issues and tagged , , , , , , , . Bookmark the permalink.

17 Responses to The Rule of Zero

  1. Gancho Manev says:

    Hello Glen,

    The link to the previous article is shortened and thus invalid. It's
    https://feabhasblog.wpengine.com/2015/01/the-rule-of-th%E2%80%A6rce-

    instead of

    https://feabhasblog.wpengine.com/2015/01/the-rule-of-the-big-four-and-a-half-move-semantics-and-resource-management/

    Best,
    Gancho

    Like (0)
    Dislike (0)
  2. Thanks Gancho. I've updated the article.

    Like (0)
    Dislike (0)
  3. sbi says:

    I think the Rule of Zero is still young enough that it reeks to talk about it without mentioning the guy who invented it: https://flamingdangerzone.com/cxx11/2012/08/15/rule-of-zero.html

    Like (0)
    Dislike (0)
  4. John M. says:

    Hi Glen,
    Shouldn't the move constructors in the first couple of examples use the move syntax (&&) rather than reference?
    for example, in CopyNotMove:
    CopyNotMove(CopyNotMove& src) = delete;
    Isn't this the copy constructor and not the move constructor? I compiled it on GCC 4.8, tried doing something like this:
    CopyNotMove cm3 = cm1;
    and the compiler complained that the copy ctor was deleted.

    Like (0)
    Dislike (0)
  5. Xeo says:

    I'll just leave this here: https://flamingdangerzone.com/cxx11/2012/08/15/rule-of-zero.html

    Like (0)
    Dislike (0)
  6. Nikos Athanasiou says:

    Wouldn't it be appropriate to give credit to R. Martinho Fernandes for coining the term? https://flamingdangerzone.com/cxx11/2012/08/15/rule-of-zero.html

    Like (0)
    Dislike (0)
  7. Lightness Races in Orbit says:

    I realise that you are a consultant, so you need to maintain the illusion that you come up with this wisdom on your own in order to justify charging absurd fees, but you could at least have taken a moment to attribute the individual responsible for the "Rule of Zero", Martinho Fernandes. Y'know, instead of just saying it "has appeared".

    And to which "whitepaper" do you refer? Do you actually mean your previous blog article?

    Like (0)
    Dislike (0)
  8. leeter says:

    I realize that you didn't plagarize this article but to the community it largely appears that you took the content from R. Martinho Fernandes article of the same name from 2012 ( https://flamingdangerzone.com/cxx11/2012/08/15/rule-of-zero.html ) I would highly encourage you to link and or credit that article to remove the appearance of impropriety

    Like (0)
    Dislike (0)
  9. bmorel says:

    Interesting readings (I have just found and read the 3 articles), it made the move semantic more clear in my mind.

    I have a question, about shared_ptr.
    What do you make about the cost that shared_ptr implies?
    Unlike unique_ptr, which should have no overhead compared to raw pointers, shared_ptr maintain a reference count, if I am not wrong. It means memory and CPU time (ok, it is not a lot, I know, and it may not be enough to justify optimizations). Not to mention that I think shared_ptr can be dangerous, because it can lead to objects not dying if it is incorrectly used.

    Would not it be more interesting if there were some kind of way to make resources managed by unique_ptr having a "T clone()const" method (through a trait, for example)? Of course, then you have to satisfy the rule of 4 and a half, even if you used unique_ptr, since unique_ptr basically just deletes the copy constructor (no traits in C++11, and also maybe a good reason to not do that trick).

    Like (0)
    Dislike (0)
  10. I absolutely agree, and I'm properly chagrined that this happened. I've updated the post to reflect this oversight.

    Like (0)
    Dislike (0)
  11. It was never my intention to claim originality for this concept; just hoping to open this up to a community that may never have come across this concept.

    Of course, I've updated the article to reflect Fernandes' original work.

    Oh, and the invoice for my absurd fee is in the post. This month I demand armadilos. 😉

    Like (0)
    Dislike (0)
  12. Agree completely; and done

    Like (0)
    Dislike (0)
  13. Lightness Races in Orbit says:

    Nice one. 🙂

    Like (0)
    Dislike (0)
  14. John M says:

    Isn't the constructor 'CopyNotMove (CopyNotMove& src)' actually a copy constructor and not a move constructor since you're using references?

    Like (0)
    Dislike (0)
  15. Well spotted! Fixed.

    Thanks

    Like (0)
    Dislike (0)
  16. Tony says:

    You shouldn't use shared_ptr together with the

    "Use a shared_ptr if you need to support copying as well as moving...

    ...This code works because the compiler generates default implementations of the copy constructor, move constructor and assignment operators for your class. These default implementations will invoke the appropriate functions on the shared_ptr..."

    The code doesn't actually work. You no longer are making *copies*. You are sharing data.

    Well, as shown the class is immutable, so sharing is equivalent to making copies, but as soon as you add a mutating function:

    OwnsMemory a(100);
    OwnsMemory b = a;

    b.setVal(200);

    assert(a.getVal() == 100); // nope! a == 200 as well!!!

    shared_ptr's should only be used for sharing.
    What we need is a clone_ptr. We don't have one yet, but there is a proposal in flight.

    Like (0)
    Dislike (0)
  17. The compiler *will* complain about your example. The line

    CopyNotMove cm3 = cm1;

    is an alternative syntax for Copy Initialisation, NOT assignment; so in effect being the same as:

    CopyNotMove cm3 { cm1 };

    That said, many compilers will elide the copy constructor and directly construct the target object (cm3). So, even though you see a 'normal' constructor being called, the copy constructor must still be available (even though it isn't used!).

    This is the compiler being efficient; if not necessarily helpful.

    Like (0)
    Dislike (0)

Leave a Reply