TimelineCpp20Concepts

Check Types with Concepts

Concepts are a powerful and elegant tool to check at compile time if a type fulfills. Thanks to static_assert, you can use concepts as a standalone feature: static_assert(Concept<T>).

 TimelineCpp20Concepts

I often have the question in my C++ class: How can I be sure that my data type is moveable? Well, you can either study the dependencies between the Big Six or define and use the concept Big Six. In my last post, “Check Types with Concepts – The Motivation“, I presented the first part of the answer and explained the very sophisticated dependencies between the Big Six. As a reminder, here are the Big Six, including move semantics:

  • Default constructor: X()
  • Copy constructor: X(const X&)
  • Copy assignment: operator = (const X&)
  • Move constructor: X(X&&)
  • Move assignment: operator = (X&&)
  • Destructor: ~(X)

Today, I want to define and use the concept Big Six.

Before I do that, I have a short disclaimer: C++20 already supports the concepts std::semiregular and std::regular.

std::semiregular and std::regular

A semiregular type has to support the Big Six and must be swappable:

  • Default constructor: X()
  • Copy constructor: X(const X&)
  • Copy assignment: operator = (const X&)
  • Move constructor: X(X&&)
  • Move assignment: operator = (X&&)
  • Destructor: ~(X)
  • Swappable: swap(X&, X&)

Additionally, std::regular requires for a type X that it supports the concept std::semiregular and is equality comparable.

  • Default constructor: X()
  • Copy constructor: X(const X&)
  • Copy assignment: operator = (const X&)
  • Move constructor: X(X&&)
  • Move assignment: operator = (X&&)
  • Destructor: ~(X)
  • Swappable: swap(X&, Y&)
  • Equality comparable: bool operator == (const X&, const X&)

That said, there is essentially no reason to define the concept BigSix. Just use the concept std::semiregular, because you get the swappable property for free. Here is a C++11 implementation of std::swap:

 

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.

     

    template <typename T>
    void swap(T& a, T& b) noexcept {
        T tmp(std::move(a));  // move constructor
        a = std::move(b);     // move assignment
        b = std::move(tmp);   // move assignment
    }
    

     

    When you invoke swap(a, b), the compiler applies move semantics to its arguments a and b. Consequentially, a type supporting the concept BigSix also supports swappable and, therefore, supports the concept std::semiregular.

    Now, let me implement the concept BigSix.

    The Concept BigSix

    Thanks to the type traits functions, implementing BigSix is a no-brainer. In the first step, I define the type traits isBigSix , and in the second step, I use it directly to define the concept BigSix. Here we are:

    // bigSixConcept.cpp
    
    #include <algorithm>
    #include <iostream>
    #include <type_traits>
    
    template<typename T>
    struct isBigSix: std::integral_constant<bool,
                                          std::is_default_constructible<T>::value &&
                                          std::is_copy_constructible<T>::value &&
                                          std::is_copy_assignable<T>::value &&
                                          std::is_move_constructible<T>::value &&
                                          std::is_move_assignable<T>::value &&
                                          std::is_destructible<T>::value>{};
    
    
    template<typename T>
    concept BigSix = isBigSix<T>::value;
    
    template <BigSix T>                                   // (1)
    void swap(T& a, T& b) noexcept {
        T tmp(std::move(a));
        a = std::move(b);
        b = std::move(tmp);
    }
    
    struct MyData{                                        // (2)
      MyData() = default;
      MyData(const MyData& ) = default;
      MyData& operator=(const MyData& m) = default;
    
    };
    
    int main(){
    
        std::cout << '\n';
    
        MyData a, b;
        swap(a, b);                                             // (3)
    
        static_assert(BigSix<MyData>, "BigSix not supported");  // (4)
    
        std::cout << '\n';
    
    }
    

     

    Now, my function swap requires that the type parameter T supports BigSix (line 1). In line 3, I invoke the function swap with arguments of type MyData. Additionally, I explicitly check in line 4 if MyData supports the concept BigSix. MyData (line 2) has a default constructor and supports copy semantics. The program can be compiled and executed.

    BigSixConcept

     

    Does this mean that MyData supports the concept BigSix and is, therefore, moved inside my function swap? Yes, MyData supports the concept BigSix, but no, MyData is not moved inside my function swap. Copy semantic kicks in as a fallback for move semantics.

    Here is a slightly modified program.

    // bigSixConceptComments.cpp
    
    #include <algorithm>
    #include <iostream>
    #include <type_traits>
    
    template<typename T>
    struct isBigSix: std::integral_constant<bool,
                                          std::is_default_constructible<T>::value &&
                                          std::is_copy_constructible<T>::value &&
                                          std::is_copy_assignable<T>::value &&
                                          std::is_move_constructible<T>::value &&
                                          std::is_move_assignable<T>::value &&
                                          std::is_destructible<T>::value>{};
    
    
    template<typename T>
    concept BigSix = isBigSix<T>::value;
    
    template <BigSix T>                                   
    void swap(T& a, T& b) noexcept {
        T tmp(std::move(a));
        a = std::move(b);
        b = std::move(tmp);
    }
    
    struct MyData{                                        
        MyData() = default;
        MyData(const MyData& ) {
            std::cout << "copy constructor\n";
        }
        MyData& operator=(const MyData& m) {
            std::cout << "copy assignment operator\n";
            return *this;
        }
    
    };
    
    int main(){
    
        std::cout << '\n';
    
        MyData a, b;
        swap(a, b);       
        
        static_assert(BigSix<MyData>, "BigSix not supported");                             
    
        std::cout << '\n';
    
    }
    

     

    I added comments to the copy constructor and copy assignment operator of MyData. Executing the program shows that both special member functions are used:

    BigSixConceptComments

    By the way, this observation is already documented in cppreference.com. For example, a note about the type trait std::is_move_constructible states: “Types without a move constructor, but with a copy constructor that accepts const T& arguments, satisfy std::is_move_constructible.

    Okay, we are back to square one. We can decide if a type supports BigSix, but we cannot decide if a type is moved. If you want to know if your type supports move semantics and not that copy semantics is used as a fallback for move semantics, you must study the dependency table of my previous post: “Check Types with Concepts – The Motivation“.

    What’s next?

    In my next post, I want to continue my story with ranges. Additionally, ranges will get many improvements in C++23.

     

     

    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)

    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 *