Namespaces
Variants

SFINAE

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

"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
  • toutes les expressions utilisées dans le type de fonction
  • toutes les expressions utilisées dans une déclaration de paramètre de modèle
  • toutes les expressions utilisées dans la liste d'arguments de modèle 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)

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 :

  • tentative d'instanciation d'une expansion de paquet contenant plusieurs paquets de longueurs différentes
(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

  • Expression mal formée utilisée dans un type de paramètre de template
  • Expression mal formée utilisée dans le type de fonction :
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.

static_assert est généralement préféré à SFINAE si seule une erreur conditionnelle à la compilation est souhaitée.

(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