Les nouveautés C++20 pour améliorer les templates en C++
Les améliorations apportées aux templates rendent le C++20 plus cohérent et, par conséquent, moins sujet aux erreurs lorsque vous écrivez des programmes génériques.
Constructeur conditionnellement explicite
Parfois, vous avez besoin d'une classe qui devrait avoir des constructeurs acceptant différents types. Par exemple, vous avez une classe DifferentType qui contient un std::variant acceptant différents types.
Exemple 1
class DifferentType { variant myVariant; };
Pour initialiser un DifferentType avec bool, char, int, double, float ou string, la classe DifferentType a besoin de constructeurs pour chaque type répertorié. Une solution paresseuse consiste à rendre le constructeur générique.
La classe Implicite présente un constructeur générique.
Exemple 2
#include <iostream> #include <string> using namespace std; struct Implicite { template <typename T> Implicite(T t) { cout << t << '\n'; } }; struct Explicite { template <typename T> explicit Explicite(T t) { cout << t << '\n'; } }; int main ( ) { Implicite imp1 = "implicite"; Implicite imp2("explicite"); Implicite imp3 = 10.5; Implicite imp4(10.5); cout << '\n'; // Explicite exp1 = "implicit"; Explicite exp2{"explicite"}; // Explicite exp3 = 2021; Explicite exp4{2021}; return 0; }
Résultat
implicite explicite 10.5 10.5 explicite 2021
Maintenant, vous avez un problème. Un constructeur générique (ligne 7) est un constructeur catch all car vous pouvez l'invoquer avec n'importe quel type. Le constructeur est bien trop gourmand. En mettant un explicite devant le constructeur (ligne 14), les conversions implicites (lignes 29 et 31) ne sont plus valides. Seuls les appels explicites (lignes 30 et 32) sont valides.
En C++20, explicite est encore plus utile. Imaginez que vous ayez un type Note qui ne devrait prendre en charge que la conversion implicite de Note, mais aucune autre conversion implicite. Dans ce cas, explicite peut être utilisé de manière conditionnelle.
Exemple 3
#include <iostream> #include <type_traits> #include <typeinfo> using namespace std; struct Note { template <typename T> explicit(!is_same<T, double>::value) Note(T t) { cout << typeid(t).name() << '\n'; } }; void afficher(Note n){ } int main ( ) { Note n1(15.50); Note n2 = 15.50; afficher(n1); afficher(17.5); // afficher(true); // afficher("Bonsoir"); return 0; }
Exemple 3
#include <iostream> #include <type_traits> #include <typeinfo> using namespace std; struct Note { template <typename T> explicit(!is_same<T, double>::value) Note(T t) { cout << typeid(t).name() << '\n'; } }; void afficher(Note n){ } int main ( ) { Note n1(15.50); Note n2 = 15.50; afficher(true); afficher("Bonsoir"); return 0; }
Résultat
D:\cplus>g++ code.cpp -o code -std=c++20 && code code.cpp: In function 'int main()': code.cpp:21:14: error: could not convert 'true' from 'bool' to 'Note' 21 | afficher(true); | ^~~~ | | | bool code.cpp:22:14: error: could not convert '(const char*)"Bonsoir"' from 'const char*' to 'Note' 22 | afficher("Bonsoir"); | ^~~~~~~~~ | | | const char*
L'expression explicit(!is_same::value) garantit que Note ne peut être implicitement créé qu'à partir d'une valeur double. La fonction is_same est un prédicat de compilation de la bibliothèque type_traits. Un prédicat au moment de la compilation, tel que is_same, est évalué au moment de la compilation et renvoie un booléen. Par conséquent, les conversions implicites de double (lignes 19 et 22) sont possibles, mais pas les conversions commentées de bool et chaines de caractères (lignes 23 et 24).
Paramètres de template sans type
C++ prend en charge les non-types (sans type) en tant que paramètres de modèle. Essentiellement, les non-types pourraient être :
- entiers et énumérateurs
- pointeurs vers des objets, des fonctions et des attributs d'une classe
- références lvalue
- std::nullptr_t
Depuis le premier standard C++, C++98, il y a eu une discussion en cours au sein de la communauté C++ sur la prise en charge des paramètres de modèle à virgule flottante. Maintenant, nous les avons et plus encore : C++20 prend en charge les virgules flottantes, les types littéraux et les littéraux de chaîne en tant que non-types.
Types à virgule flottante
Le programme suivant utilise des types à virgule flottante comme paramètres de modèle sans type.
Exemple 4
#include <iostream> #include <typeinfo> using namespace std; template <double d> auto getDouble() { return d; } template <auto NonType> auto getNonType() { return NonType; } int main ( ) { auto d1 = getDouble<5.5>(); auto d2 = getDouble<6.5>(); auto i = getNonType<2017>(); cout << i << " " << typeid(i).name() << '\n'; auto f = getNonType<2020.1f>(); cout << f << " " << typeid(f).name() << '\n'; auto d = getNonType<2020.2>(); cout << d << " " << typeid(d).name(); return 0; }
Résultat
D:\cplus>g++ code.cpp -o code -std=c++20 && code 2017 i 2020.1 f 2020.2 d
La template de fonction getDouble (ligne 6) n'accepte que les valeurs doubles. Je tiens à souligner que chaque appel du modèle de fonction getDouble (lignes 18 et 19) crée une nouvelle fonction getDouble. Cette fonction est une spécialisation complète pour la valeur double donnée. Depuis C++17, vous pouvez utiliser un paramètre de template auto comme non-type. Par conséquent, la ligne 21 est valide avec C++17. Avec C++20, vous pouvez également utiliser auto pour les types à virgule flottante. Le programme suivant visualise la déduction de type du compilateur C++20. Le compilateur déduit le type int (ligne 22), float (ligne 25) et double (ligne 28) pour le paramètre de template nontype.
Types littéraux
Les types littéraux ont les deux propriétés suivantes :
- Toutes les classes de base et les membres de données non statiques sont publics et non modifiables
- Les types de toutes les classes de base et membres de données non statiques sont des types structurels ou des tableaux de ceux-ci.
Un type littéral doit avoir un constructeur constexpr.
Exemple 5
struct NewType { constexpr NewType(int) {} }; template <NewType cl> auto getClassType() { return cl; } int main ( ) { auto c1 = getClassType<NewType(2021)>(); return 0; }
Littéraux de chaîne de caractères
Exemple 6
#include <iostream> using namespace std; template <int N> class StringLiteral { public: char data[N]; constexpr StringLiteral(char const (&str)[N]) { copy(str, str + N, data); } }; template <StringLiteral str> class ClasseTemplate {}; template <StringLiteral str> void FunctionTemplate() { cout << str.data << '\n'; } int main ( ) { ClasseTemplate<"chaine de caracteres"> cls; FunctionTemplate<"chaine de caracteres">(); return 0; }
Résultat
chaine de caracteres
StringLiteral est un type littéral et, par conséquent, peut être utilisé comme paramètre de template sans type pour ClassTemplate (ligne 15) et FunctionTemplate (ligne 18). Le constructeur constexpr (ligne 11) prend une chaîne C comme paramètre.