TimelineCpp20CoreLanguage

Various Template Improvements with C++20

Admittedly, I present in this post a few minor improvements to templates and C++20 in general. Although these improvements may not seem so impressive to you, they make C++20 more consistent and less error-prone when you program generic.

 TimelineCpp20CoreLanguage

Today’s post is about conditionally explicit constructors and new non-type template parameters.

Conditionally Explicit Constructors

Sometimes, you want to have a class that should has constructors accepting various types. For example, you have a class VariantWrapper which holds a std::variant accepting different types.

class VariantWrapper {

    std::variant<bool, char, int, double, float, std::string> myVariant;

};

 

To initialize the myVariant with bool, char, int, double, float, or std::string, the class VariantWrapper needs constructors for each listed type. Laziness is a virtue – at least for programmers – , therefore, you decide to make the constructor generic. 

The class Implicit exemplifies a generic constructor.

 

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.

     

    // explicitBool.cpp
    
    #include <iostream>
    #include <string>
    #include <type_traits>
    
    struct Implicit {
        template <typename T>        // (1)
        Implicit(T t) {
            std::cout << t << std::endl;
        }
    };
    
    struct Explicit {
        template <typename T>
        explicit Explicit(T t) {    // (2)
            std::cout << t << std::endl;
        }
    };
    
    int main() {
        
        std::cout << std::endl;
        
        Implicit imp1 = "implicit";
        Implicit imp2("explicit");
        Implicit imp3 = 1998;
        Implicit imp4(1998);
        
        std::cout << std::endl;
        
        // Explicit exp1 = "implicit";  // (3)
        Explicit exp2{"explicit"};      // (4)
        // Explicit exp3 = 2011;        // (3)
        Explicit exp4{2011};            // (4)
        
        std::cout << std::endl;  
    
    } 
    

     

    Now, you have an issue. A generic constructor (1) is a catch-all constructor because you can invoke them with any type. The constructor is way too greedy.  By putting an explicit in front of the constructor (2). the constructor becomes explicit. This means that implicit conversions (3) are not valid anymore. Only explicit calls (4) are valid.

    Thanks to Clang 10, here is the output of the program:

    explicitBool

    This is not the and of the story. Maybe, you have a type MyBool that should only support the implicit conversion from bool, but no other implicit conversion. In this case, explicit can be used conditionally.

    // myBool.cpp
    
    #include <iostream>
    #include <type_traits>
    #include <typeinfo>
    
    struct MyBool {
        template <typename T>
        explicit(!std::is_same<T, bool>::value) MyBool(T t) {  // (1)
            std::cout << typeid(t).name() << std::endl;
        }
    };
    
    void needBool(MyBool b){ }                                 // (2)
    
    int main() {
    
        MyBool myBool1(true);                              
        MyBool myBool2 = false;                                // (3)
        
        needBool(myBool1);
        needBool(true);                                        // (4)
        // needBool(5);
        // needBool("true");
        
    }
    

     

    The explicit(!std::is_same<T,  bool>::value) expression guarantees that MyBool can only be implicitly created from a bool value. The function std::is_same is a compile-time predicate from the type_traits library. Compile-time predicate means std::is_same is evaluated at compile-time and returns a boolean. Consequently, the implicit conversion from bool in (3) and (4) is possible, but not the commented-out conversions from int and a C-string.

    You are right when you argue that a conditionally explicit constructor would be possible with SFINAE. But honestly, I wouldn’t say I like the corresponding SFINAE using a constructor, because it would take me a few lines to explain it. Additionally, I only get it right after the third try.

    template <typename T, std::enable_if_t<std::is_same_v<std::decay_t<T>, bool>, bool> = true>
    MyBool(T&& t) {
        std::cout << typeid(t).name() << std::endl;
    }
    

     

    I should add a few explaining words. std::enable_if is a convenient way to use SFINAE. SFINAE stands for Substitution Failure Is Not An Error and applies during overload resolution of a function template. When substituting the template parameter fails, the specialization is discarded from the overload set but causes no compiler error. This exactly happens in this concrete case. The specialization is discarded if std::is_same_v<std::decay_t<T>, bool> evaluates to false. std::decay<T> applies conversions to T, such as removing const, volatile or a reference from T. std::decay_t<T> is a convenient syntax for std::decay<T>::type. The same holds for std::is_same_v<T, bool>, which is short for std::is_same<T, bool>::value.

    As my German reader pre alpha pointed out: the constructor using SFINAE is way too greedy. It disables all non-bool constructors.

    Beside my longish explanation, an additional argument speaks against SFINAE and for a conditionally explicit constructor: performance. Simon Brand pointed out in his post “C++20’s Conditionally Explicit Constructors“, that explicit(bool) made the template instantiation for Visual Studio 2019 about 15% faster compared to SFINAE.

    With C++20, additional non-type template parameters are supported.

    New non-type Template Parameter

    With C++20, floating points and classes with constexpr constructors are supported as non-types.

    C++ supports non-types as template parameters. Essentially non-types could be

    • integers and enumerators
    • pointer  or references to objects, functions, and to attributes of a class
    • std::nullptr_t

    When I ask the students in my class if they ever used a non-type as a template parameter, they say: No! Of course, I answer my tricky question and show an often-used example for non-type template parameters:

    std::array<int, 5> myVec;
    

     

    5 is a non-type and is used as a template argument. We are just used to it. Since the first C++-standard C++98, there is a discussion in the C++ community to support floating points as a template parameter. Now, we C++20, we have it:

     

    // nonTypeTemplateParameter.cpp
    
    struct ClassType {
        constexpr ClassType(int) {}  // (1)
    };
    
    template <ClassType cl>          // (2)
    auto getClassType() {
        return cl;
    }
    
    template <double d>              // (3)
    auto getDouble() {
        return d;
    }
    
    int main() {
    
        auto c1 = getClassType<ClassType(2020)>();
    
        auto d1 = getDouble<5.5>();  // (4)
        auto d2 = getDouble<6.5>();  // (4)
    
    }
    

     

    ClassType has a constexpr constructor (1) and can be used as a template argument (2). The same holds for the function template getDouble (3), which accepts only doubles. I want to emphasize that each call of the function template getDouble (4) with a new argument triggers the instantiation of a new function getDouble.  This means that two instantiations for the doubles 5.5 and 6.5 are created.

    If Clang would already supports this feature, I could show you with C++ Insights that each instantiation for 5.5 and 6.5 creates a fully specialized function template. At least, thanks to GCC, I can show you the relevant assembler instructions with the Compiler Explorer.

    nonTypeTemplateParameter

    The screenshot shows that the compiler created for each template argument a function.

    What’s next?

    As templates, lambdas are also improved in various ways in C++20. My next post is about these various improvements.

     

     

    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 *