Check Types with Concepts – The Motivation
static_assert
allows you to check at compile time if a type T
fulfills the Concept
: static_assert(Concept<T>)
.
Before I dive into concepts in my next post, I want to motivate their use.
When I discuss move semantics in my classes, I introduce the idea of the Big Six. The Big Six control the life cycle of objects: creation, copy, move, and destruction. Here are the six special member functions for a type X
.
- Default constructor:
X()
- Copy constructor:
X(const X&)
- Copy assignment:
operator = (const X&)
- Move constructor:
X(X&&)
- Move assignment:
operator = (X&&)
- Destructor:
~(X)
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.
If you can, you should avoid defining any default operations. 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 using types that support the appropriate copy/move semantics. This applies to the built-in types bool
or double
, but also the containers of the standard template library, 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, for example, the copy constructor for a class, it invokes the copy constructor for all class members and bases.
The fun starts when you define or
=delete
one of the special member functions because 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, sometimes excluded. Let me weaken this rule: When you define or =delete any default operation, you must consider all six. Defining a special member can mean both: You implement the particular member function, or you request it from the compiler using =default
.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
I wrote that the compiler could generate the Big Six if needed. Now, I have to clarify what I mean: This is only true if you don’t define or =delete
any special member function because there are pretty sophisticated dependencies between the six special member functions.
Here is the point when my class discussions start: How can I be sure that my type X supports move semantics? Of course, you want that your type X supports move semantics.
Move semantics has two big benefits:
- Cheap move operations are used instead of expensive copy operations
- Move operations require no memory allocation and, therefore, a
std::bad_alloc
exception is not possible
There are essentially two ways to check if a type supports the Big Six: Study the dependencies, or define and use a concept BigSix
.
First. let’s study the dependencies between the Big Six:
Dependencies between the Big Six
Howard Hinnant developed in his talk at the ACCU 2014 conference an overview of the automatically generated special member functions. Here is a screenshot of his completel table:
Howard’s table demands a profound explanation.
First of all, user-declared means for one of these six special member functions that you define it explicitly or request it from the compiler with =default
. Deletion of the special member function with =delete
is also regarded as defined. Essentially, when you use the name, 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 are affected.
When you define or delete a destructor, a copy constructor, or a copy assignment operator with =defaul
t or =delete
, you get no compiler-generated move constructor and move assignment operator. 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 red in the table. Additionally, the red-marked copy operations are deprecated.
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 a compilation error.
Due to this dependency hell, I give the following general rule in my classes: Make your user-defined types as simple as possible and go for abstraction. Let the compile do the complicated stuff.
Go for Abstraction
Here are a few consequences of this rule:
- Don’t declare any special member function if necessary.
- Use a
std::array
instead of a C-array in your class. Astd::array
supports the Big Six. - Use a
std::unique_ptr
or astd::shared_ptr
in a class, but not a raw pointer. The compiler-generated copy constructor and copy assignment operator for a raw pointer makes a shallow copy but not a deep copy. This means only the pointer is copied but not its content. Using astd::unique_ptr
orstd::shared_ptr
in a user-defined type directly expresses your intent. Astd::unique_ptr
cannot be copied; therefore, the class cannot be copied. Astd::shared_ptr
, and, therefore, the class can be copied. - If you have a user-defined type in your class that disables the auto-generation of the Big Six, you have two options. Implement the special member functions for this user-defined type, or refactor your class into two classes. Don’t let one user-defined type infect your class design.
Let me end my general rule with an anecdote: I once did a code review for a friend. He asked me to analyze his code before it went into production. He used a union in this central class. I call this class encapsulating the union for simplicity WrapperClass
. The used union was a so-called tagged union. Meaning that the WrapperClass
keeps track of the currently used type of the union. To know more about unions, read my previous post, “C++ Core Guidelines: Rules for Unions“. Finally, the WrapperClass
consisted of about 800 lines of code to support the Big Six. He had to implement the six special member functions in eight variations because the union could have eight different types. Additionally, he implemented a few convenience functions to compare instances of WrapperClass
. When I analyzed the class, it was immediately clear: this is a code smell and a reason for refactoring. I asked him if he could use C++17. The answer was yes, and I replaced the union with a std::variant. Additionally, I added a generic constructor. The result was that the WrapperClass
went from 800 to 40 lines of code. std::variant
supports the six special member functions and the six comparison operators by design.
What’s next?
You may not want to study the dependencies between the six special member functions. In my next post, I will continue this story and define and use the concept BigSix to decide at compile time if a given type supports all six special member functions.
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, Matt Godbolt, and Honey Sukesan.
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!