Various Template Improvements with C++20

Contents[Show]

Admittedly, I present in this post a few small improvements to templates and to C++20 in general. Although these improvements may seem not so impressive to you, they make C++20 more consistent and, therefore, 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 which should have constructors accepting various different types. For example, you have a class VariantWrapper which holds a std::variant accepting various 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 programmer - , therefore, you decide to make the constructor generic. 

The class Implicit exemplifies a generic constructor.

// 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 the 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 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 don't like the corresponding SFINAE using 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 think 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. It means when substituting the template parameter fails, the specialisation is discarded from the overload set but cause 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, there is an additional argument that 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 in the students in my class if they ever used a non-type as template parameter they say: No! Of course, I answer my own tricky question and show an often-used example for non-type template parameters:

std::array<int, 5> myVec;

 

5 is a non-type and 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, therefore, be used as a template argument (2). The same holds for the function template getDouble (3) which accepts only doubles. I want to emphasize is explicit, that each call of the function template getDouble (4) with a new argument triggers the instantiation of a new function getDouble.  This means that there are two instantiations for the doubles 5.5 and 6.5 are created.

If Clang would already support 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?

Such 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: Meeting C++, Matt Braun, Roman Postanciuc, Venkata Ramesh Gudpati, Tobias Zindl, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Darshan Mody, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Wolfgang Gärtner, Jon Hess, Christian Wittenhorst, Louis St-Amour, Stephan Roslen, Venkat Nandam, Jose Francisco, Douglas Tinkham, Lakshman, Kuchlong Kuchlong, Avi Kohn, Serhy Pyton, Robert Blanch, Kuma [], Truels Wissneth, Kris Kafka, Mario Luoni, and Neil Wang. 

Seminars

I'm happy to give online-seminars or face-to-face seminars world-wide. Please call me if you have any questions.

Standard Seminars 

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

Contact Me

Modernes C++,

RainerGrimmSmall

 

Comments   

0 #1 FederAndInk 2020-07-29 20:30
What's cool also is that you can do explicit(false) to say explicitly that it is an implicit ctor or cast
Quote

My Newest E-Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 6844

Yesterday 10139

Week 37885

Month 6844

All 4627738

Currently are 196 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments