The Rule of Zero, or Six

The Rule of Zero, or Six

This post is a cross-post from www.ModernesCpp.com.

The rule of zero, or six, is one of the advanced rules in modern C++. I wrote in my current book "C++ Core Guidelines Explained: Best Practices for Modern C++" about them. Today, I want to quote the relevant parts of my book in this post.

No alt text provided for this image
Cippie reasons about the rule of zero, five, or six.


  

 











By default, the compiler can generate the big six if needed. You can define the six special member functions, but can also ask explicitly the compiler to provide them with = default or delete them with = delete.

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

This rule is also known as "the rule of zero". That means, that you can avoid writing any custom constructor, copy/move constructors, assignment operators, or destructors by using types that support the appropriate copy/move semantics. This applies to the regular types such as the built-in types bool or double, but also the containers of the Standard Template Library (STL)such as std::vector or std::string.

class Named_map {
 public:
    // ... no default operations declared ...
 private:
    std::string name;
    std::map<int, int> rep;
};

Named_map nm;       // default construct
Named_map nm2{nm};  // copy construct        

The default construction and the copy construction work because they are already defined for std::string and std::map. When the compiler auto-generates the copy constructor for a class, it invokes the copy constructor for all members and all bases of the class.

C.21: If you define or =delete any default operation, define or =delete them all

The big six are closely related. Due to this relation, you have to define or =delete all six. Consequently, this rule is called "the rule of six". Sometimes, you hear "the rule of five", because the default constructor is special, and, therefore, sometimes excluded.

Dependencies between the special member functions

Howard Hinnant developed in his talk at the ACCU 2014 conference an overview to the automatically generated special member functions.

No alt text provided for this image
Automatically generated special member functions

Howard's table demands a deep explanation.

  • First of all, user-declared means for one of these six special member functions that you define it explicitly or auto request it from the compiler with =default. Deletion of the special member function with =delete is also regarded as user declared. Essentially, when you just use the name, such the name of the default constructor, it counts as user declared.
  • When you define any constructor, you get no default constructor. A default constructor is a constructor which can be invoked without an argument.
  • When you define or delete a default constructor with =default or =delete, no other of the six special member functions is affected.
  • When you define or delete a destructor, a copy constructor, or a copy-assignment operator with =default or =delete, you get no compiler-generated move-constructor and move-assignment constructor. This means move operations such as move construction or move assignment fall back to copy operations such as copy construction or copy assignment. This fallback automatism is marked in red in the table.
  • When you define or delete with =default or =delete a move constructor or a move assignment operator, you get only the defined =default or =delete move constructor or move assignment operator. Consequently, the copy constructor and the copy assignment operator are set to =delete. Invoking a copy operation such as copy construction or copy assignment causes, therefore, a compilation error.

When you don't follow this rule, you get very unintuitive objects. Here is an unintuitive example from the guidelines.

// doubleFree.cpp

#include <cstddef>

class BigArray {

 public:
    BigArray(std::size_t len): len_(len), data_(new int[len]) {}

    ~BigArray(){
        delete[] data_;
    }

 private:
  size_t len_;
  int* data_;
};

int main(){
  
    BigArray bigArray1(1000);
    
    BigArray bigArray2(1000);
    
    bigArray2 = bigArray1;    // (1)

}                             // (2)        

Why does this program have undefined behavior? The default copy assignment operation bigArray2 = bigArray1 (1) of the example copies all members of bigArray2. Copying means, in particular, that pointer data is copied but not the data. Hence, the destructor for bigArray1 and bigArray2 is called (2), and we get undefined behavior because of double free.

The unintuitive behavior of the example is, that the compiler-generated copy assignment operator of BigArray makes a shallow copy of BigArray, but the explicitly implemented destructor of BigArray assumes ownership of data.

AddressSanitizer makes the undefined behavior visible.

No alt text provided for this image
Double free detected with Address Sanitizer
No alt text provided for this image

Modernes C++ Mentoring

Stay Informed: Subscribe Here.

C.22 Make default operations consistent

This rule is related to the previous rule. If you implement the default operations with different semantics, the users of the class may become very confused. This strange behavior may also appear if you partially implement the member functions and partially request them via =default. You cannot assume that the compiler-generated special member functions have the same semantics as yours.

As an example of the odd behavior, here is the class Strange. Strange includes a pointer to int.

 
// strange.cpp

#include <iostream>

struct Strange { 
  
    Strange(): p(new int(2011)) {}
    
    // deep copy 
    Strange(const Strange& a) : p(new int(*a.p)) {}                 // (1)
  
    // shallow copy
    // equivalent to Strange& operator = (const Strange&) = default;
    Strange& operator = (const Strange& a) {                        // (2)           
        p = a.p;
        return *this;
    }  
   
    int* p;
    
};

int main() {
  
    std::cout << '\n';
  
    std::cout << "Deep copy" << '\n';
  
    Strange s1;
    Strange s2(s1);                                               // (3)                                   
  
    std::cout << "s1.p: " << s1.p << "; *s1.p: " << *s1.p << '\n';
    std::cout << "s2.p: " << s2.p << "; *s2.p: " << *s2.p << '\n';
  
    std::cout <<  "*s2.p = 2017" << '\n';
    *s2.p = 2017;                                                  // (4)                             
  
    std::cout << "s1.p: " << s1.p << "; *s1.p: " << *s1.p << '\n';
    std::cout << "s2.p: " << s2.p << "; *s2.p: " << *s2.p << '\n';
  
    std::cout << '\n';
  
    std::cout << "Shallow copy" << '\n';

    Strange s3;
    s3 = s1;                                                       // (5)                                    
  
    std::cout << "s1.p: " << s1.p << "; *s1.p: " << *s1.p << '\n';
    std::cout << "s3.p: " << s3.p << "; *s3.p: " << *s3.p << '\n';
  
  
    std::cout <<  "*s3.p = 2017" << '\n';
    *s3.p = 2017;                                                  // (6)                               
  
    std::cout << "s1.p: " << s1.p << "; *s1.p: " << *s1.p << '\n';
    std::cout << "s3.p: " << s3.p << "; *s3.p: " << *s3.p << '\n';
  
    std::cout << '\n';
  
    std::cout << "delete s1.p" << '\n';                            // (7)  
    delete s1.p;                                        
  
    std::cout << "s2.p: " << s2.p << "; *s2.p: " << *s2.p << '\n'; // (8) 
    std::cout << "s3.p: " << s3.p << "; *s3.p: " << *s3.p << '\n';
  
    std::cout << '\n';
  
}        

The class Strange has a copy constructor (1) and a copy-assignment operator (2). The copy constructor applies deep copy, and the assignment operator applies shallow copy. By the way, the compiler-generated copy constructor or copy assignment operator also applies shallow copy. Most of the time, you want deep copy semantics (value semantics) for your types, but you probably never want to have different semantics for these two related operations. The difference is that deep copy semantics creates two new separate storage p(new int(*a.p)) while shallow copy semantics just copies the pointer p = a.p. Let’s play with the Strange types. The following figure shows the output of the program.

No alt text provided for this image
Output of strange.cpp

Line 3 uses the copy constructor to create s2. Displaying the addresses of the pointer and changing the value of the pointer s2.p (line 4) shows that s1 and s2 are two distinct objects. This is not the case for s1 and s3. The copy-assignment operation in line (5) performs a shallow copy. The result is that changing the pointer s3.p (line 6) also affects the pointer s1.p because both pointers refer to the same value.

The fun starts if I delete the pointer s1.p (line 7). Thanks to the deep copy, nothing bad happens to s2.p, but the value of s3.p becomes an invalid pointer. To be more precise: Dereferencing an invalid pointer such as in *s3.p (line 8) is undefined behavior.

What's Next?

The idea of a regular type is deeply embodied in the Standard Template Library (STL). The idea goes back to Alexander Stephanov, who is the creator of the STL. Let me write more about regular types in my next post.


Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Evangelos Denaxas, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Matthieu Bolt, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, and Phillip Diekmann.

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, Ralf Abramowitsch, John Nebel, Mipko, and Alicja Kaminska.

My special thanks to Embarcadero

My special thanks to PVS-Studio

Seminars

I'm happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

Bookable (Online)

German

Standard Seminars (English/German)

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.


New

Contact Me

Modernes C++

To view or add a comment, sign in

More articles by Rainer Grimm

  • Last Chance: 1 Day Left

    Last Chance: 1 Day Left

    December 19, 2024 Make the Difference Let’s do something great together: From December 1st to 24th, when you book one…

  • std::execution: More Senders

    std::execution: More Senders

    offers three types of senders: factories, adapters, and consumers. I’ll take a closer look at these today.

  • Christmas Special – 5 Days Left

    Christmas Special – 5 Days Left

    Make the Difference Let’s do something great together: From December 1st to 24th, when you book one of my mentoring…

  • Christmas Special – 7 Days Left

    Christmas Special – 7 Days Left

    Make the Difference Let’s do something great together: From December 1st to 24th, when you book one of my mentoring…

  • std::execution: Sender

    std::execution: Sender

    std::execution offers three types of senders: factories, adapters, and consumers. I’ll take a closer look at these…

  • My ALS Journey (18/n): C++ and ALS

    My ALS Journey (18/n): C++ and ALS

    Christmas Promotion Let’s do something great together: From December 1st to 24th, when you book one of my mentoring…

  • std::execution: Composition of Senders

    std::execution: Composition of Senders

    Most sender adaptors are composable using the pipe operator. Let me start with a simple example of composition with the…

  • std::execution: Inclusive Scan

    std::execution: Inclusive Scan

    Before I present the asynchronous inclusive scan, I introduce the inclusive scan, aka prefix sum. Prefix Sum In…

  • std::execution: Asynchronous Algorithms

    std::execution: Asynchronous Algorithms

    supports many asynchronous algorithms for various workflows. Presenting proposal P2300R10 is not easy.

  • My ALS Journey (17/n): Christmas Special

    My ALS Journey (17/n): Christmas Special

    Today, I have a special Christmas gift. My ALS Journey so far Make the Difference Let’s do something great together:…

Insights from the community

Others also viewed

Explore topics