Dans le billet précédent, nous avons utilisé une assertion statique. Ces assertions sont faites lors de la compilation: si elles échouent, le programme ne compile pas et –et c'est là l'intérêt principal– le compilateur affiche un message clair, que vous avez défini, plutôt qu'une longue suite d'erreurs template illisibles. La STL C++11 définit un certain nombre de conditions qui peuvent être utilisées dans une expression static_assert. std::is_same
Is it a good processor?
Prenons une fonction mineTree, par exemple, qui prend pour argument un Processor qui doit posséder un opérateur de fonction applicable à un std::pair
La signature de notre condition sera: template
Nous pourrons l'utiliser de la façon suivante:
static_assert(has_func_operator
"Bad Processor Error: Processor must implement func operator with signature operator()(Arg)");
au début de la fonction mineTree.
Qu'est-ce qu'une condition statique?
static_assert exige une condition statique, c'est-à-dire une condition qu'il est possible de vérifier à la compilation. Il est donc impossible d'écrire, par exemple:
char c;
std::cin >> c;
static_assert(c == 'a', "erreur: c != a"); // c'est déterminé à l'exécution !
Donc toute la difficulté de l'exercice est d'obtenir l'information sans entrer dans un contexte d'exécution, appelé aussi contexte d'évaluation. Prenons l'exemple de la condition std::is_same, comment peut-elle être implémentée? Assez simplement, en fait, même avec les versions plus anciennes du standard: on utilise la possibilité de spécialisation partielle des templates, processus qui se déroule entièrement à la compilation:
template
struct is_same {
static const bool value = false; // T et U sont des types différents
};
template
struct is_same
static const bool value = true; // donc is_same::value = true
};
Hélas, tout n'est pas si simple
Certaines conditions sont plus difficiles à vérifier que d'autres. Celle que nous recherchons, has_func_operator, ne peut pas être implémentée uniquement avec les spécialisations partielles. On peut de cette façon vérifier le type d'une fonction, mais pas son existence: pour que la spécialisation fonctionne, il faut qu'au moins une des spécialisations soit valide. Il faut trouver une façon d'utiliser le contexte de compilation d'une façon que l'erreur devienne constructive –et c'est exactement le rôle de cette technique nommée SFINAE.
Substitution failure is not an error
L'échec d'une substitution n'est pas une erreur. Décortiquons cela:
l'échec d'une substitution: pour instancier une fonction template surchargée (avec plusieurs signatures), le compilateur regarde les différentes signatures possibles et choisit celle qui est la plus adaptée. C'est le principe de la substitution: on substitue à une signature générique une signature déterminée.
n'est pas une erreur: vous me direz que c'est la même chose pour une fonction normale, sans template: certes, mais avec une différence importante: si une des fonctions normales qui peut être choisie est mal formée, le compilateur refusera de compiler. Ce n'est pas le cas lorsqu'il s'agit d'une fonction template. Pourquoi? Parce qu'une fonction template qui n'est pas appelée n'est pas instanciée. Pour le compilateur, elle n'existe pas. Donc si elle est mal formée, peu importe -> l'échec d'une substitution n'est pas une erreur.
Concrètement, comment ça marche?
Comme une fonction template qui n'est pas retenue lors de l'étape de substitution n'est pas instanciée, deux fonctions de même nom peuvent être surchargées aussi bien du côté des arguments que du côté du type de retour. En examinant le type de retour, on peut donc savoir quelle surcharge a été appelée. C'est ainsi qu'on utilisait SFINAE dans les versions du standard antérieures à C++11. Par exemple, voici une astuce pour déterminer si un type est une classe. Elle repose sur le fait qu'une signature comportant un pointeur sur un membre non statique d'un type provoquera un échec de substitution si le type n'est pas une classe:
typedef char is_a_class; // on différencie les types de retour par leur taille
typedef char is_not_a_class; // on est au moins sûr que sizeof(char) == 1
template
template
La moitié du chemin
Nous avons fait la moitié du chemin, reste la deuxième. Comme vous pouvez le constater, les signatures de func ci-dessus ne sont pas définies. Ce n'est pas gênant, car nous devons rester en dehors du contexte d'évaluation ou d'instanciation. Avant C++11, le moyen de rester dans ce contexte était offert par l'opérateur sizeof. C'est la raison pour laquelle j'ai pris deux types de retours dont on peut être certain qu'ils sont de tailles différentes. Nous allons pouvoir encapsuler notre résolution de substitution et résoudre la question de la surcharge retenue:
template
struct is_class {
// comme avant
typedef char is_a_class;
typedef char is_not_a_class;
template
template
// et on rajoute
static const int value = sizeof(func
};
De retour au processeur et à C++11
L'implémentation de SFINAE qu'on a vue est très astucieuse, mais c'est de l'histoire ancienne. C++11 offre des ressources plus puissantes pour la métaprogrammation. C'est avec ces ressources nouvelles que nous résoudrons la question initiale, l'écriture de has_func_operator. En voici le code, l'explication vient:
// 1
template
auto constexpr has_func_operator_intern(int) -> decltype(std::declval
return true;
}
// 2
template
bool constexpr has_func_operator_intern(...) {
return false;
}
// 3
template
struct has_func_operator {
static const bool value = has_func_operator_intern
};
Nous commençons par la deuxième fonction:
La première fonction est plus compliquée:
La troisième fonction est toute simple: c'est seulement une enveloppe autour des deux premières qui évite d'utiliser directement SFINAE en écrivant:
has_func_operator_intern
dans le corps du programme. De plus, elle harmonise l'interface de notre condition statique avec l'interface des conditions proposées par la STL.
En conclusion
Dans notre contexte, SFINAE n'a permis qu'une seule chose: générer un message d'erreur plus lisible si le Processor fourni n'a pas les fonctionnalités suffisantes. Mais ses possibilités sont nombreuses. À vous, maintenant que vous avez l'idée en tête, de faire preuve d'imagination! Au fur et à mesure que vous entrerez dans les subtilités de SFINAE, vous découvrirez aussi les subtilités du C++: savoir ce qui appartient au contexte d'évaluation (où tout doit être défini) et au contexte de compilation (où les définitions partielles sont permises) est une question byzantine. Vous pouvez jeter un œil sur cppreference pour débroussailler le terrain. Vous verrez que tant qu'on reste en dehors de l'usage «odr» (comprendre one definition rule,) on reste dans les limites de ce qui peut être réalisé à la compilation.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.