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.
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.
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 |
---|---|
|
|
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?
À ACCU 2018, comme rapporté par Peter Somerlad, Timur Doumler a soumis cette intéressante recommandation : « Don't use deduction guides if the deduced argument(s) would not be obvious from the call site ». L'idée est qu'il est facile d'obscurcir le propos à l'aide de CTAD, alors mieux vaut – selon lui – choisir les cas d'utilisation de manière à alléger la syntaxe, et non pas de manière à la rendre opaque.
Permettez-moi de vous présenter cette application de techniques de métaprogrammation à l'aide de CTAD, que j'ai emprunté (avec mineures retouches) à Manu Sánchez (source originale). Portez particulièrement attention aux assertions statiques, qui profitent de l'application de la métafonction eval() :
// Une fonction
template<class R>
struct Function {
template<class Tag, class... Args>
constexpr Function(Tag, Args...) {
}
using type = R;
};
// Un type
template<class T>
struct Type {
constexpr Type() = default;
using type = T;
};
template<class T, class U>
constexpr bool operator==(const Type<T> &, const Type<U> &) {
return std::is_same_v<T, U>;
}
template<class T, class U>
constexpr bool operator!=(const Type<T> &a, const Type<U> &b) {
return !(a == b);
}
// « Fabrique » de types
template<class T>
constexpr Type<T> type {};
// Opération pour évaluer une fonction
template<class Function>
constexpr auto eval(Function) {
return type<typename Function::type>;
}
// Le passage amusant débute ici
struct AddConst {};
struct AddPointer {};
template<class T>
Function(AddConst, Type<T>)->Function<const T>;
template<class T>
Function(AddPointer, Type<T>)->Function<T *>;
constexpr auto constInt = eval(Function(AddConst{}, type<int>));
constexpr auto intPtr = eval(Function(AddPointer{}, type<int>));
static_assert(type<const int> == constInt);
static_assert(type<int *> == intPtr);
int main() {
}
Quelques liens pour enrichir le propos.