Verschiedene Template-Verbesserungen mit C++20
Zugegeben, ich stelle in dem heutigen Artikel ein paar kleine Verbesserungen zu Templates und zu C++20 im Allgemeinen vor. Obwohl diese Verbesserungen nicht so beeindruckend wirken, wird durch sie C++20 konsistenter und damit auch weniger fehleranfällig, wenn es um das generische Programmieren geht.
Zugegeben, ich stelle in dem heutigen Artikel ein paar kleine Verbesserungen zu Templates und zu C++20 im Allgemeinen vor. Obwohl diese Verbesserungen nicht so beeindruckend wirken, wird durch sie C++20 konsistenter und damit auch weniger fehleranfällig, wenn es um das generische Programmieren geht.
![Verschiedene Template Verbesserungen mit C++20](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/2/9/4/1/7/9/9/TimelineCpp20CoreLanguage-8a896849d78febdc.png)
Der heutige Artikel befasst sich mit dem bedingt expliziten Konstruktor und neuen Nichttyp (non-type) Template-Parameter.
Bedingt expliziter Konstruktor
Häufig soll eine Klasse mehrere Konstruktoren besitzen, die verschiedene Datentypen annehmen kann. Zum Beispiel gilt dies für die Klasse VariantWrapper
, die eine std::variant
enthält.
class VariantWrapper {
std::variant<bool, char, int, double, float, std::string> myVariant;
};
Um myVariant
mit bool, char, int, double, float
oder std::string
zu initialisieren, benötigt die Klasse VariantWrapper
Konstruktoren für jeden der aufgezählten Datentypen. Faulheit ist eine Tugend - zumindest für Programmierer. Daher bietet sich in diesem Fall ein generischer Konstruktor an.
Die Klasse Implicit
stellt einen generischen Konstruktor vor.
// 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;
}
Diese Umsetzung besitzt ein Problem. Ein generischer Konstruktor (1) ist ein Catch-all-Konstruktor, da er mit jedem beliebigen Datentyp verwendet werden kann. Der Konstruktor ist viel zu gierig. Durch die Verwendung des Schlüsselworts explicit
(1) vor dem Konstruktor wird dieser explizit. Das heißt, das er keine impliziten Konvertierungen (3) ausführt. Lediglich explizite Aufrufe (4) sind gültig.
Dank dem Clang 10 Compiler ist hier die Ausgabe des Programms:
![Verschiedene Template Verbesserungen mit C++20](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/2/9/4/1/7/9/9/explicitBool-84b899a3f12c53f9.png)
Dies ist aber noch nicht das Ende der Geschichte. Eventuell möchte man einen Datentyp MyBool
implementieren, der lediglich die implizite Konvertierung von bool
erlaubt. Alle anderen impliziten Konvertierung sollen aber nicht zulässig sein. In diesem Fall lässt sich explicit
auch bedingt verwenden.
// 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); // (5)
// needBool("true"); // (5)
}
Der explicit(!std::is_same<T, bool>::value
) Ausdruck sichert zu, dass MyBool
nur implizit von einem bool
-Wert erzeugt werden kann. Die Funktion std::is_same
ist ein Compile-Zeit-Prädikat der Type-Traits Bibliothek [1]. Compilezeit-Prädikat bedeutet, dass std::is_same
beim Kompilieren ausgeführt wird und einen Wahrheitswert zurückgibt. Konsequenterweise sind die impliziten Konvertierung von bool
in (3) und (4) zulässig. Diese implizite Konvertierung für einen int
- und einen C-String-Wert ist aber nicht in Zeile (5) zulässig.
Nun könnte man einwenden, dass der bedingt explizite Konstruktor sich auch mit SFINAE [2] umsetzen ließe. Ehrlich gesagt bin ich kein Freund des entsprechenden Konstruktors, der SFINAE verwendet. Zum einen benötige ich mehrere Sätze, um ihn zu erklären, zum anderen habe ich ihn erst nach dem dritten Versuch richtig implementiert.
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;
}
Ich denke, jetzt sind ein paar erklärende Worte notwendig. SFINAE steht für Substitution Failure Is Not An Error und wird während der Überladung von Funktions-Templates angewandt. Es bedeutet: Wenn bei der Ersetzung der Template-Parameter ein Fehler auftritt, ist dies kein Fehler. Diese Instantiierung der Template-Parameter wird aus der Menge aller Funktionsüberladungen entfernt. Genau das passiert in dem konkreten Fall. Die Spezialisierung wird entfernt, wenn std::is_same_v<std::decay_t<T>, bool>
zu false
evaluiert. std::decay<T>
wendet Konvertierung auf T
wie das Entfernen von const
, volatile
oder das Entfernen einer Referenz von T
an. std::decay_t<T>
ist eine angenehme Schreibeweise für std::decay<T>::type
. Dasselbe gilt für std::is_same_v<T, bool>
als Kurzform für std::is_same<T, bool>::value
.
Pre alpha wies mich darauf hin, dass dieser SFINAE einsetzende Konstruktor viel zu gierig ist: Er unterbindet alle Konstruktoren, die nicht bool verwenden.
Neben meiner länglichen Erklärung gibt es ein weiteres Argument, das gegen SFINAE und für den bedingten expliziten Konstruktor spricht: Performanz. Simon Brand hat in seinem Artikel "C++20's Conditionally Explicit Constructors [3]" dargestellt, dass Template Instanziierung mit explicit(bool)
im Falle von Visual Studio 2019 um 15% schneller ist als die Template Instanziierung mit SFINAE.
Mit C++20 werden auch weitere Nichttyp (non-type) Template-Parameter unterstützt.
Neue Nichttyp Template-Parameter
Mit C++20 werden Fließkommazahlen und Klassen mit constexpr
Konstruktor als Nichttypen unterstützt.
C++ unterstützt bereits Nichttypen als Template-Parameter. Im Wesentlichen sind Nichttypen
- Ganzzahlen oder Aufzähler
- Zeiger oder Referenzen auf Objekte, Funktionen oder Mitglieder einer Klasse
std::nullptr_t
Wenn ich die Teilnehmer meiner Schulung frage, ob sie bereits Nichttypen als Template-Parameter verwendet haben, ist die einheitliche Antwort: Nein! Natürlich beantworte ich direkt meine knifflige Frage und stelle ein oft verwendetes Beispiel für Nichttyp Template-Parameter vor.
std::array<int, 5> myVec;
5 ist ein Nichttyp und wird als Template-Argument verwendet. Seit dem ersten Standard C++98 gibt es die Diskussion in der C++-Community, dass Fließkommazahlen auch als Template-Parameter unterstützt werden sollten. Mit C++20 stehen sie zur Verfügung.
// 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
besitzt einen constexpr
Konstruktor (1) und kann daher als Template-Argument verwendet werden (2). Dieselbe Beobachtung gilt für das Funktions-Template getDouble
(3), das nur double
-Werte annimmt. Ich möchte es explizt betonen, dass jeder Aufruf des Funktions-Templates getDouble
(4) mit einem neuen Argument die Instanziierung einer Funktion getDouble
bewirkt. Das bedeutet, dass zwei Instanziierung für die double
-Werte 5.5 und 6.5 erzeugt werden.
Wenn der Clang-Compiler dieses Feature bereits unterstützen würde, könnte ich schön mit C++ Insights zeigen, dass jede Instanziierung für 5.5 und 6.5 ein vollständig spezialisiertes Funktions-Template erzeugen würde. Zumindest kann ich aber dank des GCC-Compilers und dem Compiler Explorer [4] die entscheidenden Assemblerbefehle vorstellen.
![Verschiedene Template Verbesserungen mit C++20](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/2/9/4/1/7/9/9/nonTypeTemplateParameter-bc6268f87859e5cd.png)
Der Screenshot zeigt, dass der Compiler für jedes Template-Argument eine Funktion erzeugt.
Wie geht's weiter?
Ähnlich wie Templates erfahren Lambdas auch einige Verbesserungen in C++20. In meinem nächsten Artikel gehe ich auf diese Verbesserungen ein. ( [5])
URL dieses Artikels:
https://www.heise.de/-4852396
Links in diesem Artikel:
[1] https://en.cppreference.com/w/cpp/header/type_traits
[2] https://en.cppreference.com/w/cpp/language/sfinae
[3] https://devblogs.microsoft.com/cppblog/c20s-conditionally-explicit-constructors/
[4] https://godbolt.org/
[5] mailto:rainer@grimm-jaud.de
Copyright © 2020 Heise Medien