Déduction des types de classes générique – Class Type Argument Deduction (CTAD)

Prenons un cas simple (du moins en apparence), soit celui du type std::pair<T,U> qui représente une paire de valeurs. Ce type sert à plusieurs endroits dans la bibliothèque standard, notamment pour représenter les paires {clé,valeur} d'une std::map :

#include <map>
#include <string>
#include <iostream>
#include <iterator>
#include <fstream>
int main() {
   using namespace std;
   map<string,int> mots;
   ifstream in{ "z.cpp" }; // fichier à ouvrir
   for(string s; in >> s;)
      ++mots[s]; // compter le nb. occurrences de chaque "mot"
   for(pair<string, int> &&p : mots) // prendre chaque paire de la map
      cout << p.first << " : " << p.second << '\n'; // nom : nb. occurrences
}

Ici, nous aurions pu remplacer for(pair<string,int> &&p : mots) par for(auto &&p : mots) bien entendu, mais l'intention était de présenter clairement la forme que prend l'écriture du type. Notez que pour initialiser une telle paire « manuellement », nous aurions pu utiliser l'une des écritures suivantes :

pair<string,int> p0 { "Allo"s, 3 };
auto p1 = pair<string,int> { "Allo"s, 3 };
auto p2 = make_pair("Allo"s, 3);

Remarquez en particulier la troisième option, qui repose sur une fonction de fabrication. L'intérêt de cette écriture est en partie qu'étant une fonction, ses paramètres sont évalués d'abord (pour fins de déduction de la surcharge la plus appropriée), ce qui permet de déduire leurs types et de ne pas avoir à les expliciter; dans le cas des constructeurs (deux premières options), les types T et U d'un pair<T,U> font partie du type de la variable et doivent traditionnellement être explicités à même les sources du programme.

Cette écriture peut être extrêmement douloureuse, Comparez par exemple ce qui suit :

int f(double,void*); // fonction
// ...
pair<string,int(*)(double,void*)> p0 { "f()"s, f };
auto p1 = pair<string,int(*)(double,void*)> { "f()"s, f };
auto p2 = make_pair("f()"s, f);

Visiblement, la fonction de fabrication entraîne un allègement de l'écriture. Dans certains cas, par exemple celui des expressions λ, exprimer une paire sans fonction de fabrication peut rapidement devenir acrobatique.

Ces fonction de fabrication sont souvent triviales à exprimer. Par exemple, make_pair() peut s'écrire (dans sa forme la plus naïve) ainsi :

template <class T, class U>
   pair<T, U> make_pair(T t, U t) {
      return { t, u };
   }

La multiplication de telles fonctions dans le standard en est venue, au fil des années, à faire réfléchir certains quant à l'intérêt de remplacer le Boilerplate (écriture triviale et quelque peu redondante) par un mécanisme plus direct. Ainsi, grâce en particulier à Mike Spertus, il est devenu possible avec C++ 17 d'écrire tout simplement ceci :

int f(double,void*); // fonction
// ...
pair p0 { "f()"s, f }; // simple, direct

C'est ce que nous nommons le Class Template Argument Deduction, ou CTAD.

Comment fonctionne CTAD

Ce que fait CTAD pour un type générique est de déduire les paramètres du type générique à partir des types des objets passés à la construction d'une instance. Par exemple, avec un type comme Paire<T,U> (version simplifiée de std::pair<T,U>) ci-dessous :

template <class T, class U>
   struct Paire {
      T premier;
      U second;
      Paire(T premier, U second) : premier{ premier }, second{ second } {
      }
   };

... le code client suivant :

Paire p0{ 3, 3.5 };

... déduira que T est int et que U est double.

Pour les cas plus nichés : guides de déduction (Deduction Guides)

Il arrive que les intentions ne soient pas immédiatement déductibles des types impliqués. Prenons le cas où l'on souhaiterait appeler un constructeur de séquence :

Code traditionnel Tentative naïve avec CTAD
#include <list>
#include <vector>
#include <iterator>
int main() {
   using namespace std
   list<double> lst{ 1.5. 2.5, 3.5 };
   vector<double> v{ begin(lst), end(lst) }; // Ok, constructeur de séquence
   // ...
}
#include <list>
#include <vector>
#include <iterator>
int main() {
   using namespace std
   list lst{ 1.5. 2.5, 3.5 }; // Ok, list<double>
   vector v{ begin(lst), end(lst) }; // Oups! A priori, c'est un
                                     // vector<list<double>::iterator>
   // ...
}

Ici, l'intention dans le code de droite était probablement de copier les trois valeurs de lst dans v, mais le résultat de la déduction de types réalisé par CTAD seul mènerait à un vecteur de deux itérateurs de list<double>.

Un autre cas où CTAD pourrait donner un résultat malheureux serait celui où le type souhaité n'est pas immédiatement évident sur la base du type utilisé pour construire l'objet. Par exemple :

template <class T*>
class S {
   T obj;
public:
   S(const T &obj) : obj{ obj } {
   } 
};
S s = "allo"; // T est const char*; et si nous voulions string, que faire?

Pour de tels cas, il existe ce que l'on nomme des guides de déduction. Ces guides sont des règles qui guident le compilateur dans son processus de déduction. En pratique, CTAD applique des règles de déduction implicites, qu'il est possible de raffiner, de spécialiser par des guides de déduction explicites.

Pour le cas du vecteur construit à partie d'une paire d'itérateurs, un guide de déduction possible serait :

#include <list>
#include <vector>
#include <iterator>
//
// guide de déduction
//
template <class It> vector(It, It) -> vector<typename iterator_traits<It>::value_type>;
int main() {
   using namespace std
   list lst{ 1.5. 2.5, 3.5 }; // Ok, list<double>
   vector v{ begin(lst), end(lst) }; // bingo! vector<double>
   // ...
}

Dans le cas de la déduction de std::string à partir de const char*, un guide de déduction possible serait :

#include <string>
template <class T*>
class S {
   T obj;
public:
   S(const T &obj) : obj{ obj } {
   } 
};
//
// guide de déduction
//
S(const char*) -> S<std::string>;
S s = "allo"; // bingo! S<string>!

Charmant, n'est-ce pas?

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !