TimelineCpp20Concepts

Concepts in C++20: An Evolution or a Revolution?

Let me conclude my series to concepts with this meta-post. Are concepts an evolution or a revolution in C++? The answer to this question bothered me quite a time.

 TimelineCpp20Concepts

I assume we all know what evolution or revolution means, but let me be more precise. These definitions are quite concise:

  • Evolution is defined as gradual change, adaptation, progression, and metamorphosis.
  • Revolution is the forcible overthrow of an entirely new system…drastic, disruptive, far-reaching, meaningful change.

To make it short. The crucial difference between evolution and revolution is if the change is gradual (evolution) or disruptive (revolution).

I had many discussions in the series about concepts. Consequentially, I was curious about what your opinion of concepts is. The answers I got are in German. For your convenience, I paraphrase it in English. Interestingly, my readers have a clear tendency to evolve. Honestly, I’m more on the revolution side.

Which argument speaks for evolution, and which argument speaks for revolution?

Evolution

evolution 4107273 1280

Image by Alexas_Fotos from Pixabay

 

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.

     

    Clear Abstraction 

    When appropriately used, concepts should promote clean working with generic code at a higher level of abstraction.

    In the longer term, the standard concepts should become increasingly idiomatic in C++. The interoperability and modular work, especially in larger teams, can be made more robust and less prone to errors if more abstract properties of the parameter classes are checked and less for purely syntactic “rollout” in generic code.

    Simple Definition and Meaningful Error Messages

    You cannot do anything that you would not have been able to do with type-traits library, SFINAE, and static_assert so far, even if it was very cumbersome and time-consuming. Their advantages lie in the simple definition and meaningful error messages.

    Unconstrained Placeholders

    Since C++11, we have auto to deduce the type of a variable from its initializer.

    auto integ = add(2000, 11);
    
    std::vector<int> myVec{1, 2, 3};
    for (auto v: myVec) std::cout << v << std::endl;
    

    auto is a kind of unconstrained placeholder. With C++20, we can use concepts as constrained placeholders.

     

    template<typename T>                                   
    concept Integral = std::is_integral<T>::value;
    
    Integral auto integ = add(2000, 11);
    
    std::vector<int> myVec{1, 2, 3};
    for (Integral auto v: myVec) std::cout << v << std::endl;
    

     

    To make it concise and evolutionary: you can use a constrained placeholder (concept) in each place you could use an unconstrained placeholder (auto).

    Generic Lambdas

     With C++14, you can use a generic lambda (addLambda), which essentially becomes a function template (addTemplate).

    // addLambdaGeneric.pp
    
    #include <iostream>
    
    auto addLambda = [](auto fir, auto sec){ return fir + sec; }; 
    
    template <typename T, typename T2>                            
    auto addTemplate(T fir, T2 sec){ return fir + sec; }
    
    
    int main(){
        
        std::cout << addLambda(2000, 11.5);    // 2011.5
        std::cout << addTemplate(2000, 11.5);  // 2011.5
        
    }
    

     

    Using auto in a function declaration was not possible with C++14. Since C++20, you can use a constrained placeholder (concept) or an unconstrained placeholder (auto) in a function declaration, and the function declaration becomes a function template with unconstrained (auto) or constrained (concept) parameters.

    // addUnconstrainedConstrained.cpp
    
    #include <concepts>
    #include <iostream>
    
    auto addUnconstrained(auto fir, auto sec){
         return fir + sec;
    }
    
    std::floating_point auto addConstrained(std::integral auto fir, 
                                            std::floating_point auto sec){
         return fir + sec;
    }
    
    int main(){
        
        std::cout << addUnconstrained(2000, 11.5); // 2011.5
        std::cout << addConstrained(2000, 11.5);   // 2011.5
        
    }
    

     

    To make my point, I intentionally used a strange signature for my addConstrained function. 

    Revolution

    france 63022 1280

     

    Image by WikiImages from Pixabay

    Template Requirements are verified

    Admittedly, you can specify template requirements in C++11 in the template declaration.

    // requirementsCheckSFINAE.cpp
    
    #include <type_traits>
    
    template<typename T,
             typename std::enable_if<std::is_integral<T>::value, T>:: type = 0>
    T moduloOf(T t) {
        return t % 5;
    }
    
    int main() {
    
        auto res = moduloOf(5.5);
    
    }
    

     

    The function template moduloOf requires that the T has to be integral. If T is not integral and the expression std::is_integral<T>::value evaluates to false, the failed substitution is not an error. The compiler removes the overload from the set of all potential overloads. After that step, there is no valid overload left.

    SFINAE

    This technique is called SFINAE and stands for Substitution Failure Is Not An Error. 

    Honestly, I only teach this technique in template classes. This does not hold for concepts. The issue becomes immediately obvious.

    // requirementsCheckConcepts.cpp
    
    #include <concepts>
    
    std::integral auto moduloOf(std::integral auto t) {
        return t % 5;
    }
    
    int main() {
    
        auto res = moduloOf(5.5);
    
    }
    

     

     conceptsError

     

    The Definition of Templates radically improved

    Thanks to the abbreviated function template syntax, the definition of a function template becomes a piece of cake. I already presented the new syntactic sugar in the function declarations of addConstrained, and moduloOf. Therefore, I skip the example.

    Semantic Categories

    Concepts do not stand for syntactic constraints but for semantic categories. 

    Addable is a concept that stands for a syntactic constraint.

     

    template<typename T>
    concept Addable = has_plus<T>;    // bad; insufficient
    
    template<Addable N> auto algo(const N& a, const N& b) // use two numbers
    {
        // ...
        return a + b;
    }
    
    int x = 7;
    int y = 9;
    auto z = algo(x, y);   // z = 16
    
    std::string xx = "7";
    std::string yy = "9";
    auto zz = algo(xx, yy);   // zz = "79"
    

     

    I assume Addable behaves not like expected. The function template algo should accept arguments that model numbers and not just support the + operator. Consequentially, two strings can be used as arguments. This is bad because addition is commutative, but not string concatenation: “7” + “9” != “9” + “7”.

    The solution is quite simple. Define the concept Number. Number is a semantic category such as Equal, Callable, Predicate, or Monad are.

    My Conclusion

    Of course, many arguments speak for an evolutionary step or a revolutionary jump in concepts. Mainly because of the semantic categories, I’m on the revolution side. Concepts such as Number, Equality, or Ordering remind me of Platon’s words of ideas. It is revolutionary for me that we can now reason about programming in such categories. 

    What’s next?

    The ranges library that I will present in the next post, is the first customer of concepts. Ranges support algorithms that can

    • operate directly on the container
    • be evaluated lazily
    • be composed

     

     

     

    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 *