TimelineCpp20Concepts

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>).

 TimelineCpp20Concepts

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.

Rainer D 6 P2 500x500

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • 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:

    DefaultDelete

     

    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 =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 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 makes 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; 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 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)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *