SFINAE
"L'échec de substitution n'est pas une erreur"
Cette règle s'applique lors de la résolution de surcharge des modèles de fonction : Quand
la substitution
du type spécifié explicitement ou
déduit
pour le paramètre de modèle échoue, la spécialisation est écartée de l'
ensemble de surcharge
au lieu de provoquer une erreur de compilation.
Cette fonctionnalité est utilisée dans la métaprogrammation de templates.
Table des matières |
Explication
Les paramètres du modèle de fonction sont substitués (remplacés par les arguments du modèle) deux fois :
- les arguments de modèle explicitement spécifiés sont substitués avant la déduction des arguments de modèle
- les arguments déduits et les arguments obtenus à partir des valeurs par défaut sont substitués après la déduction des arguments de modèle
La substitution se produit dans
- tous les types utilisés dans le type de fonction (ce qui inclut le type de retour et les types de tous les paramètres)
- tous les types utilisés dans les déclarations de paramètres template
- tous les types utilisés dans la liste d'arguments template d'une spécialisation partielle
|
(depuis C++11) |
|
(depuis C++20) |
Un échec de substitution est toute situation où le type ou l'expression ci-dessus serait mal formé (avec un diagnostic requis), s'il était écrit en utilisant les arguments substitués.
Seuls les échecs dans les types et expressions du contexte immédiat du type de fonction ou de ses paramètres de template ou de son spécificateur explicit (depuis C++20) sont des erreurs SFINAE. Si l'évaluation d'un type/expression substitué entraîne un effet secondaire tel que l'instanciation d'une spécialisation de template, la génération d'une fonction membre implicitement définie, etc., les erreurs dans ces effets secondaires sont traitées comme des erreurs irrécupérables. Une expression lambda n'est pas considérée comme faisant partie du contexte immédiat. (depuis C++20)
|
Cette section est incomplète
Raison : mini-exemple où cela est important |
La substitution procède dans l'ordre lexical et s'arrête lorsqu'un échec est rencontré.
|
S'il existe plusieurs déclarations avec des ordres lexicaux différents (par exemple, un modèle de fonction déclaré avec un type de retour final, à substituer après un paramètre, et redéclaré avec un type de retour ordinaire qui serait substitué avant le paramètre), et que cela entraînerait des instanciations de modèles dans un ordre différent ou pas du tout, alors le programme est mal formé ; aucun diagnostic requis. |
(depuis C++11) |
template<typename A> struct B { using type = typename A::type; }; template< class T, class U = typename T::type, // Échec SFINAE si T n'a pas de membre type class V = typename B<T>::type> // erreur grave si B n'a pas de membre type // (garanti de ne pas se produire via CWG 1227 car // la substitution dans l'argument template par défaut // de U échouerait d'abord) void foo (int); template<class T> typename T::type h(typename B<T>::type); template<class T> auto h(typename B<T>::type) -> typename T::type; // redéclaration template<class T> void h(...) {} using R = decltype(h<int>(0)); // mal formé, aucun diagnostic requis
SFINAE sur les types
Les erreurs de type suivantes sont des erreurs SFINAE :
|
(since C++11) |
- tentative de création d'un tableau de void, d'un tableau de référence, d'un tableau de fonction, d'un tableau de taille négative, d'un tableau de taille non intégrale, ou d'un tableau de taille zéro :
template<int I> void div(char(*)[I % 2 == 0] = nullptr) { // cette surcharge est sélectionnée lorsque I est pair } template<int I> void div(char(*)[I % 2 == 1] = nullptr) { // cette surcharge est sélectionnée lorsque I est impair }
-
tentative d'utilisation d'un type à gauche d'un opérateur de résolution de portée
::et ce n'est pas une classe ou une énumération :
template<class T> int f(typename T::B*); template<class T> int f(T); int i = f<int>(0); // utilise la seconde surcharge
- tentative d'utilisation d'un membre d'un type, où
-
- le type ne contient pas le membre spécifié
- le membre spécifié n'est pas un type là où un type est requis
- le membre spécifié n'est pas un template là où un template est requis
- le membre spécifié n'est pas un non-type là où un non-type est requis
template<int I> struct X {}; template<template<class T> class> struct Z {}; template<class T> void f(typename T::Y*) {} template<class T> void g(X<T::N>*) {} template<class T> void h(Z<T::template TT>*) {} struct A {}; struct B { int Y; }; struct C { typedef int N; }; struct D { typedef int TT; }; struct B1 { typedef int Y; }; struct C1 { static const int N = 0; }; struct D1 { template<typename T> struct TT {}; }; int main() { // L'inférence échoue dans chacun de ces cas : f<A>(0); // A ne contient pas de membre Y f<B>(0); // Le membre Y de B n'est pas un type g<C>(0); // Le membre N de C n'est pas une non-type h<D>(0); // Le membre TT de D n'est pas un template // L'inférence réussit dans chacun de ces cas : f<B1>(0); g<C1>(0); h<D1>(0); } // todo : doit démontrer la résolution de surcharge, pas seulement l'échec
- tentative de création d'un pointeur vers une référence
- tentative de création d'une référence vers void
- tentative de création d'un pointeur vers membre de T, où T n'est pas un type classe :
template<typename T> class is_class { typedef char yes[1]; typedef char no[2]; template<typename C> static yes& test(int C::*); // sélectionné si C est un type classe template<typename C> static no& test(...); // sélectionné autrement public: static bool const value = sizeof(test<T>(nullptr)) == sizeof(yes); };
- tentative d'attribuer un type invalide à un paramètre de template constant :
template<class T, T> struct S {}; template<class T> int f(S<T, T()>*); struct X {}; int i0 = f<X>(0); // à faire : doit démontrer la résolution de surcharge, pas seulement l'échec
- tentative d'effectuer une conversion non valide dans
-
- dans une expression d'argument de template
- dans une expression utilisée dans la déclaration de fonction :
template<class T, T*> int f(int); int i2 = f<int, 1>(0); // ne peut pas convertir 1 en int* // todo: doit démontrer la résolution de surcharge, pas seulement l'échec
- tentative de création d'un type de fonction avec un paramètre de type void
- tentative de création d'un type de fonction qui retourne un type tableau ou un type fonction
Expression SFINAE
|
Seules les expressions constantes utilisées dans les types (telles que les bornes de tableau) devaient être traitées comme SFINAE (et non comme des erreurs fatales) avant C++11. |
(until C++11) |
|
Les erreurs d'expression suivantes sont des erreurs SFINAE
struct X {}; struct Y { Y(X){} }; // X is convertible to Y template<class T> auto f(T t1, T t2) -> decltype(t1 + t2); // overload #1 X f(Y, Y); // overload #2 X x1, x2; X x3 = f(x1, x2); // deduction fails on #1 (expression x1 + x2 is ill-formed) // only #2 is in the overload set, and is called |
(since C++11) |
SFINAE dans les spécialisations partielles
La déduction et la substitution interviennent également lors de la détermination si une spécialisation d'un modèle de classe ou de variable (since C++14) est générée par une spécialisation partielle ou par le modèle principal. Un échec de substitution n'est pas traité comme une erreur fatale lors de cette détermination, mais entraîne l'ignorance de la déclaration de spécialisation partielle correspondante, comme dans la résolution de surcharge impliquant des modèles de fonction.
// le modèle principal gère les types non référençables : template<class T, class = void> struct reference_traits { using add_lref = T; using add_rref = T; }; // la spécialisation reconnaît les types référençables : template<class T> struct reference_traits<T, std::void_t<T&>> { using add_lref = T&; using add_rref = T&&; }; template<class T> using add_lvalue_reference_t = typename reference_traits<T>::add_lref; template<class T> using add_rvalue_reference_t = typename reference_traits<T>::add_rref;
Support de la bibliothèque
|
Le composant de la bibliothèque standard std::enable_if permet de créer un échec de substitution afin d'activer ou de désactiver des surcharges particulières basées sur une condition évaluée au moment de la compilation. De plus, de nombreux traits de type doivent être implémentés avec SFINAE si les extensions de compilateur appropriées ne sont pas disponibles. |
(depuis C++11) |
|
Le composant de la bibliothèque standard std::void_t est une autre métafonction utilitaire qui simplifie les applications SFINAE de spécialisation partielle. |
(depuis C++17) |
Alternatives
Lorsque cela est applicable,
la distribution par étiquette
,
if constexpr
(depuis C++17)
, et les
concepts
(depuis C++20)
sont généralement préférés à l'utilisation de SFINAE.
|
|
(depuis C++11) |
Exemples
Un idiome courant consiste à utiliser le SFINAE par expression sur le type de retour, où l'expression utilise l'opérateur virgule, dont la sous-expression gauche est celle qui est examinée (convertie en void pour garantir que l'opérateur virgule défini par l'utilisateur sur le type retourné n'est pas sélectionné), et la sous-expression droite a le type que la fonction est censée retourner.
#include <iostream> // This overload is added to the set of overloads if C is // a class or reference-to-class type and F is a pointer to member function of C template<class C, class F> auto test(C c, F f) -> decltype((void)(c.*f)(), void()) { std::cout << "(1) Class/class reference overload called\n"; } // This overload is added to the set of overloads if C is a // pointer-to-class type and F is a pointer to member function of C template<class C, class F> auto test(C c, F f) -> decltype((void)((c->*f)()), void()) { std::cout << "(2) Pointer overload called\n"; } // This overload is always in the set of overloads: ellipsis // parameter has the lowest ranking for overload resolution void test(...) { std::cout << "(3) Catch-all overload called\n"; } int main() { struct X { void f() {} }; X x; X& rx = x; test(x, &X::f); // (1) test(rx, &X::f); // (1), creates a copy of x test(&x, &X::f); // (2) test(42, 1337); // (3) }
Sortie :
(1) Class/class reference overload called (1) Class/class reference overload called (2) Pointer overload called (3) Catch-all overload called
Rapports de défauts
Les rapports de défauts modifiant le comportement suivants ont été appliqués rétroactivement aux normes C++ précédemment publiées.
| DR | Appliqué à | Comportement tel que publié | Comportement correct |
|---|---|---|---|
| CWG 295 | C++98 |
la création d'un type de fonction cv-qualifié
pouvait entraîner un échec de substitution |
rendu non échec,
en ignorant la qualification cv |
| CWG 1227 | C++98 | l'ordre de substitution n'était pas spécifié | identique à l'ordre lexical |
| CWG 2054 | C++98 | la substitution dans les spécialisations partielles n'était pas correctement spécifiée | spécifiée |
| CWG 2322 | C++11 |
les déclarations dans des ordres lexicaux différents entraîneraient
des instanciations de template dans un ordre différent ou pas du tout |
un tel cas est mal formé,
aucun diagnostic requis |