adplus-dvertising

Les nouveautés C++20 pour améliorer les templates en C++

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.

Partager ce cours avec tes amis :
Rédigé par ESSADDOUKI Mostafa
ESSADDOUKI
The education of the 21st century opens up opportunities to not merely teach, but to coach, mentor, nurture and inspire.