C++ Core Guidelines: The Rule of Zero, Five, or Six
This post is about the rule of zero, five, or maybe six. I will also show the difference between copy and reference semantics and a quite similar topic: deep versus shallow copy.
To be precise, C++ has about 50 rules for managing the lifecycle of an object. This time I will write about the three fundamental default operation rules. I provide you with the link to each of the rules of the C++ core guidelines. If necessary, you can read the details following the link. Let’s start.
C++ provides six default operations, sometimes called special functions, for managing the lifecycle of an object. Consequently, this first post to the lifecycle of objects has to start with the six operations.
- a default constructor:
X()
- a copy constructor:
X(const X&)
- a copy assignment:
operator=(const X&)
- a move constructor:
X(X&&)
- a move assignment:
operator=(X&&)
- a destructor:
~X()
The default operations are related. This means if you implement or =delete one of them, you have to think about the five others. The word implement may seem a bit confusing. For the default constructor, it means that you can define it or request it from the compiler:
X(){}; // explicitly defined X() = default; // requested from the compiler
This rule also holds for the five other default operations.
One general remark before I write about the set of default operations rules. C++ provides value semantics and not reference semantics for its types. Here is the best definition I found of both terms from https://isocpp.org/wiki/faq/value-vs-ref-semantics.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
- Value semantic: Value (or “copy”) semantics mean assignment copies the value, not just the pointer.
- Reference semantic: With reference semantics, the assignment is a pointer-copy (i.e., a reference).
Here are the first three rules:
Set of default operations rules:
- C.20: If you can avoid defining any default operations, do
- C.21: If you define or
=delete
any default operation, define or=delete
them all - C.22: Make default operations consistent
C.20: If you can avoid defining any default operations, do
This rule is also known as “the rule of zero“. That means you are done if your class needs no default operations because all its members have the six special functions.
struct Named_map { public: // ... no default operations declared ... private: string name; map<int, int> rep; }; Named_map nm; // default construct Named_map nm2 {nm}; // copy construct
The default and copy construction will work because they are already defined for std::string and std::map.
C.21: If you define or =delete
any default operation, define or =delete
them all
Because we have to define or =delete all six of them, this rule is called “the rule of five“. Five seems strange to me. The reason for the rule of five or six is quite apparent. The six operations are closely related; therefore, the probability is high that you will get odd objects if you don’t follow the rule. Here is an example from the guidelines.
struct M2 { // bad: incomplete set of default operations public: // ... // ... no copy or move operations ... ~M2() { delete[] rep; } private: pair<int, int>* rep; // zero-terminated set of pairs }; void use() { M2 x; M2 y; // ... x = y; // the default assignment // ... }
What is strange about this example? First, the destructor deletes rep, which was never initialized. Second, and that is more serious. The default copy assignment operation (x = y) in the last line copies all members of M2. This means, in particular, that the pointer rep will be copied. Hence, the destructor for x and y will be called, and we get undefined behavior because of double deletion.
C.22: Make default operations consistent
This rule is related to the previous rule. If you implement the default operations with different semantics, the class users may be confused. This is the reason I constructed the class Strange. To observe the odd behavior, Strange includes a pointer to int.
// strange.cpp (https://github.com/RainerGrimm/ModernesCppSource)
#include <iostream> struct Strange{ Strange(): p(new int(2011)){} // deep copy Strange(const Strange& a) : p(new int(*(a.p))){} // (1) // shallow copy Strange& operator=(const Strange& a){ // (2) p = a.p; return *this; } int* p; }; int main(){ std::cout << std::endl; std::cout << "Deep copy" << std::endl; Strange s1; Strange s2(s1); // (3) std::cout << "s1.p: " << s1.p << "; *(s1.p): " << *(s1.p) << std::endl; std::cout << "s2.p: " << s2.p << "; *(s2.p): " << *(s2.p) << std::endl; std::cout << "*(s2.p) = 2017" << std::endl; *(s2.p) = 2017; // (4) std::cout << "s1.p: " << s1.p << "; *(s1.p): " << *(s1.p) << std::endl; std::cout << "s2.p: " << s2.p << "; *(s2.p): " << *(s2.p) << std::endl; std::cout << std::endl; std::cout << "Shallow copy" << std::endl; Strange s3; s3 = s1; // (5) std::cout << "s1.p: " << s1.p << "; *(s1.p): " << *(s1.p) << std::endl; std::cout << "s3.p: " << s3.p << "; *(s3.p): " << *(s3.p) << std::endl; std::cout << "*(s3.p) = 2017" << std::endl; *(s3.p) = 2017; // (6) std::cout << "s1.p: " << s1.p << "; *(s1.p): " << *(s1.p) << std::endl; std::cout << "s3.p: " << s3.p << "; *(s3.p): " << *(s3.p) << std::endl; std::cout << std::endl; std::cout << "delete s1.p" << std::endl; delete s1.p; // (7) std::cout << "s2.p: " << s2.p << "; *(s2.p): " << *(s2.p) << std::endl; std::cout << "s3.p: " << s3.p << "; *(s3.p): " << *(s3.p) << std::endl; std::cout << std::endl; }
The class Strange has a copy constructor (1) and a copy assignment operator (2). The copy constructor uses deep copy, and the assignment operator uses shallow copy. You often 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 semantic creates two separated new objects (p(new int(*(a.p)) while shallow copy semantic copies the pointer (p = a.p). Let’s play with the Strange types. Here is the output of the program.
I use the copy constructor in the expression (3) to create s2. Displaying the pointer’s addresses and changing the pointer’s value s2.p (4) shows that s1 and s2 are two distinct objects. That will not hold for s1 and s3. The copy assignment in expression (5) triggers a shallow copy. The result is that changing the pointer s3.p (6) will also affect the pointer s1.p; therefore, both pointers have the same value.
The fun starts if I delete the pointer s1.p (7). Because of the deep copy, nothing bad happened to s2.p; but the value becomes s3.p a null pointer. To be more precise: dereferencing a null pointer, such as in (*s3.p), is undefined behavior.
What’s next
The story of the C++ core guidelines for the lifecycle of objects goes on. It continues with the rules for the destruction of objects. This is also my plan for the 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, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, 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, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery,and Matt Godbolt.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!