Déduction d'argument de modèle de classe


La norme C ++ 17 a ajouté une nouvelle fonctionnalité au langage: Déduction d'arguments de modèle de classe (CTAD) . Avec de nouvelles fonctionnalités en C ++, traditionnellement ajouté de nouvelles façons de tirer sur leurs propres membres. Dans cet article, nous comprendrons ce qu'est le CTAD, à quoi il sert, comment il simplifie la vie et quels pièges il contient.


Commençons de loin


Rappelez-vous à quoi sert la déduction d'argument de modèle et à quoi elle sert. Si vous vous sentez suffisamment en confiance avec les modèles C ++, vous pouvez ignorer cette section et passer immédiatement à la suivante.


Avant C ++ 17, la sortie des paramètres de modèle s'appliquait uniquement aux modèles de fonction. Lors de l'instanciation d'un modèle de fonction, vous ne pouvez pas spécifier explicitement les arguments de modèle qui peuvent être déduits des types des arguments de fonction réels. Les règles de déduction sont assez compliquées, elles sont couvertes dans toute la section 17.9.2 de la norme [temp.deduct] (ci-après je me réfère à la version librement disponible du projet de norme ; dans les versions futures, la numérotation des sections peut changer, donc je recommande de rechercher par le code mnémonique spécifié dans crochets).


Nous n'analyserons pas en détail toutes les subtilités de ces règles; elles ne sont nécessaires qu'aux développeurs de compilateurs. Pour une utilisation pratique, il suffit de se rappeler une règle simple: le compilateur peut dériver indépendamment les arguments du modèle de fonction, si cela peut être fait sans ambiguïté sur la base des informations disponibles. Lors de la dérivation de types de paramètres de modèle, les transformations standard sont appliquées comme lors de l'appel d'une fonction régulière ( const est rejeté des types littéraux, les tableaux sont réduits en pointeurs, les références de fonction sont réduites en pointeurs de fonction, etc.).


template <typename T> void func(T t) { // ... } int some_func(double d) { return static_cast<int>(d); } int main() { const int i = 123; func(i); // func<int> char arr[] = "Some text"; func(arr); // func<char *> func(some_func); // func<int (*)(double)> return 0; } 

Tout cela simplifie l'utilisation des modèles de fonction, mais, hélas, est complètement inapplicable aux modèles de classe. Lors de l'instanciation des modèles de classe, tous les paramètres de modèle non par défaut devaient être spécifiés explicitement. En lien avec cette propriété désagréable, toute une famille de fonctions libres avec le préfixe make_ est apparue dans la bibliothèque standard: make_unique , make_shared , make_pair , make_tuple , etc.


 //  auto tup1 = std::tuple<int, char, double>(123, 'a', 40.0); //   auto tup2 = std::make_tuple(123, 'a', 40.0); 

Nouveau en C ++ 17


Dans la nouvelle norme, par analogie avec les paramètres des modèles de fonction, les paramètres des modèles de classe sont dérivés des arguments des constructeurs appelés:


 std::pair pr(false, 45.67); // std::pair<bool, double> std::tuple tup(123, 'a', 40.0); // std::tuple<int, char, double> std::less l; // std::less<void>,     std::less<> l template <typename T> struct A { A(T,T); }; auto y = new A{1, 2}; //  A<int> auto lck = std::lock_guard(mtx); // std::lock_guard<std::mutex> std::copy_n(vi1, 3, std::back_insert_iterator(vi2)); //       template <typename T> struct F { F(T); } std::for_each(vi.begin(), vi.end(), Foo([&](int i) {...})); // F<lambda> 

Il convient immédiatement de mentionner les restrictions CTAD qui s'appliquent au moment de C ++ 17 (peut-être que ces restrictions seront supprimées dans les futures versions de la norme):


  • CTAD ne fonctionne pas avec les alias de modèle:

 template <typename X> using PairIntX = std::pair<int, X>; PairIntX p{1, true}; //   

  • CTAD n'autorise pas la sortie partielle d'arguments (comment cela fonctionne-t-il pour la déduction d'argument modèle standard):

 std::pair p{1, 5}; // OK std::pair<double> q{1, 5}; // ,   std::pair<double, int> r{1, 5}; // OK 

En outre, le compilateur ne pourra pas déduire les types de paramètres de modèle qui ne sont pas explicitement liés aux types d'arguments constructeur. L'exemple le plus simple est un constructeur de conteneur qui accepte une paire d'itérateurs:


 template <typename T> struct MyVector { template <typename It> MyVector(It from, It to); }; std::vector<double> dv = {1.0, 3.0, 5.0, 7.0}; MyVector v2{dv.begin(), dv.end()}; //     T   It 

Le type It n'est pas directement lié à T , bien que nous, les développeurs, sachions exactement comment l'obtenir. Afin d'indiquer au compilateur comment générer des types directement non liés, une nouvelle construction de langage est apparue en C ++ 17 - le guide de déduction , dont nous discuterons dans la section suivante.


Guides de dédicace


Pour l'exemple ci-dessus, le guide de déduction ressemblerait à ceci:


 template <typename It> MyVector(It, It) -> MyVector<typename std::iterator_traits<It>::value_type>; 

Ici, nous indiquons au compilateur que pour un constructeur avec deux paramètres du même type, vous pouvez déterminer le type de T en utilisant la construction std::iterator_traits<It>::value_type . Veuillez noter que les guides de déduction sont en dehors de la définition de classe, cela vous permet de personnaliser le comportement des classes externes, y compris les classes de la bibliothèque standard C ++.


Une description formelle de la syntaxe des guides de déduction est donnée dans C ++ Standard 17 dans la section 17.10 [temp.deduct.guide] :


 [explicit] template-name (parameter-declaration-clause) -> simple-template-id; 

Le mot clé explicite avant le guide de déduction interdit de l'utiliser avec la copie-liste-initialisation :


 template <typename It> explicit MyVector(It, It) -> MyVector<typename std::iterator_traits<It>::value_type>; std::vector<double> dv = {1.0, 3.0, 5.0, 7.0}; MyVector v2{dv.begin(), dv.end()}; //  MyVector v3 = {dv.begin(), dv.end()}; //   

Soit dit en passant, le guide de déduction ne doit pas nécessairement être un modèle:


 template<class T> struct S { S(T); }; S(char const*) -> S<std::string>; S s{"hello"}; // S<std::string> 

Algorithme CTAD détaillé


Les règles formelles de dérivation des arguments de modèle de classe sont décrites en détail dans la clause 16.3.1.8 [over.match.class.deduct] de la norme C ++ 17. Essayons de les comprendre.


Nous avons donc un modèle de type C pour lequel CTAD est appliqué. Afin de choisir quel constructeur et avec quels paramètres appeler, pour C , beaucoup de fonctions modèles sont formées selon les règles suivantes:


  • Pour chaque constructeur Ci , une fonction de modèle Fi factice est générée. Les paramètres du modèle Fi sont des paramètres C , suivis des paramètres du modèle Ci (le cas échéant), y compris des paramètres avec des valeurs par défaut. Les types de paramètres de la fonction Fi correspondent aux types de paramètres du constructeur Ci . Renvoie une fonction factice Fi type C avec des arguments correspondant aux paramètres du modèle C.

Pseudocode:


 template <typename T, typename U> class C { public: template <typename V, typename W = A> C(V, W); }; //    template <typename T, typename U, typename V, typename W = A> C<T, U> Fi(V, W); 

  • Si le type C n'est pas défini ou qu'aucun constructeur n'est spécifié, les règles ci-dessus s'appliquent au constructeur hypothétique C () .
  • Une fonction factice supplémentaire est générée pour le constructeur C © ; pour cela, ils ont même proposé un nom spécial: candidat à déduction de copie .
  • Pour chaque guide de déduction , une fonction fictive Fi est également générée avec des paramètres de modèle et des arguments de guide de déduction et une valeur de retour correspondant au type à droite de -> dans le guide de déduction (dans la définition formelle, il est appelé simple-template-id ).

Pseudocode:


 template <typename T, typename V> C(T, V) -> C<typename DT<T>, typename DT<V>>; //    template <typename T, typename V> C<typename DT<T>, typename DT<V>> Fi(T,V); 

De plus, pour l'ensemble résultant de fonctions factices Fi , les règles habituelles de sortie des paramètres de modèle et de la résolution de surcharge sont appliquées à une exception près: lorsqu'une fonction factice est appelée avec une liste d'initialisation composée d'un seul paramètre de type cv U , où U est la spécialisation C ou un type hérité de la spécialisation C (juste au cas où, je préciserai que cv == const volatile ; un tel enregistrement signifie que les types U , const U , volatile U et const volatile U sont traités de la même manière), la règle qui donne la priorité au constructeur C(std::initializer_list<>) (est ignorée pour plus de détails sur la liste initia se trouve dans la clause 16.3.1.7 [over.match.list] de la norme C ++ 17). Un exemple:


 std::vector v1{1, 2}; // std::vector<int> std::vector v2{v1}; // std::vector<int>,   std::vector<std::vector<int>> 

Enfin, s'il a été possible de choisir la seule fonction fictive la plus appropriée, le constructeur ou le guide de déduction correspondant est sélectionné. S'il n'y en a pas ou s'il en existe plusieurs également, le compilateur signale une erreur.


Pièges


CTAD est utilisé pour initialiser des objets, et l'initialisation est traditionnellement une partie très déroutante du langage C ++. Avec l'ajout d' une initialisation uniforme en C ++ 11, les moyens de tirer sur votre jambe n'ont fait qu'augmenter. Vous pouvez maintenant appeler le constructeur d'un objet avec des crochets ronds et bouclés. Dans de nombreux cas, ces deux options fonctionnent de la même manière, mais pas toujours:


 std::vector v1{8, 15}; // [8, 15] std::vector v2(8, 15); // [15, 15, … 15] (8 ) std::vector v3{8}; // [8] std::vector v4(8); //   

Jusqu'à présent, tout semble être assez logique: v1 et v3 appellent le constructeur qui prend std::initializer_list<int> , int est déduit des paramètres; v4 ne peut pas trouver un constructeur qui accepte un seul paramètre de type int . Mais ce sont encore des fleurs, des baies devant:


 std::vector v5{"hi", "world"}; // [“hi”, “world”] std::vector v6("hi", "world"); // ?? 

v5 , comme prévu, sera de type std::vector<const char*> et initialisé avec deux éléments, mais la ligne suivante fait quelque chose de complètement différent. Pour un vecteur, il n'y a qu'un seul constructeur qui prend deux paramètres du même type:


 template< class InputIt > vector( InputIt first, InputIt last, const Allocator& alloc = Allocator() ); 

grâce au guide de déduction pour std::vector "hi" et "world" seront traités comme des itérateurs, et tous les éléments situés "entre" seront ajoutés à un vecteur de type std::vector<char> . Si nous avons de la chance et que ces deux constantes de chaîne sont en mémoire consécutives, alors trois éléments tomberont dans le vecteur: 'h', 'i', '\ x00', mais, très probablement, un tel code entraînera une violation de la protection de la mémoire et un plantage du programme.


Matériaux utilisés


Projet de norme C ++ 17
CTAD
CppCon 2018: Stephan T. Lavavej "Déduction d'argument de modèle de classe pour tout le monde"

Source: https://habr.com/ru/post/fr461963/


All Articles