Check Types with Concepts - The Motivation

Check Types with Concepts - The Motivation

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

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 by 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 the example the copy constructor for a class, it invokes the copy constructor for all members and all bases of the class.

The fun start, 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, therefore, sometimes excluded. Let me weaken this rule a bit: When you define or =delete any default operation, you have to think about all six. Defining a special member can mean both: You implement the special member function, or you request it from the compiler using =default.

I wrote that the compiler can 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 exactly the point when the discussions in my classes 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:

No alt text provided for this image

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 request it from the compiler with =default. Deletion of the special member

function with =delete is also regarded as defined. Essentially, when you just 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 =default 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, therefore, 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. A std::array supports the Big Six.
  • Use a std::unique_ptr or a std::shared_ptr in a class, but not a raw pointer. The compiler-generated copy constructor and copy assignment operator for a raw pointer make a shallow copy but not a deep copy. This means only the pointer is copied, but not its content. Using a std::unique_ptr or std::shared_ptr in a user-defined type directly expresses your intent. A std::unique_ptr cannot be copied, and, therefore, the class cannot be copied. A std::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 goes 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. If you want 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. Essentially, 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 can 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 lines of code to 40 lines of code. std::variant supports the six special member functions and the six comparison operators by design.

What's next?

Maybe, you don't want to study the dependencies between the six special member functions. In my next post, I 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, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, Louis St-Amour, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, 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, Eduardo Velasquez, 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, Ralf Holly, and Juan Dent.

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

My special thanks to Embarcadero

My special thanks to PVS-Studio

 

Mentoring Program

My new mentoring program "Fundamentals for C++ Professionals" starts on the 22nd of April. Get more information here: https://bit.ly/MentoringProgramModernesCpp.

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