Aller au contenu

SFINAE, type traits et enable_if

Comprenez le principe « Substitution Failure Is Not An Error », utilisez les type traits pour l'introspection à la compilation et contraignez les templates avec enable_if.

Qu'est-ce que SFINAE ?

SFINAE signifie Substitution Failure Is Not An Error (un échec de substitution n'est pas une erreur). Lorsque le compilateur tente d'instancier un template, il substitue les types déduits ou spécifiés dans le template. Si cette substitution produit un type ou une expression invalide dans le contexte immédiat de la déclaration du template, le compilateur n'émet pas d'erreur — il retire simplement ce template de l'ensemble des surcharges et passe aux autres candidats.

Ce mécanisme est le fondement de la métaprogrammation par templates en C++11/14. Il vous permet d'écrire des templates disponibles sous condition, selon les propriétés de leurs arguments de type.

L'expression clé est « contexte immédiat ». Les échecs de substitution survenant au plus profond du corps d'une fonction sont des erreurs fatales (hard errors), pas du SFINAE. Seuls les échecs dans la signature de la fonction (type de retour, types des paramètres, liste des paramètres de template ou clause requires) déclenchent SFINAE.

std::enable_if

std::enable_if définit un type membre type égal à T uniquement si Condition vaut true. Si Condition vaut false, le membre type n'existe pas, ce qui provoque un échec de substitution (SFINAE). L'alias std::enable_if_t est un raccourci pour typename std::enable_if::type.

enable_if.cpp
#include <iostream>
#include <type_traits>

// Only enabled for integral types
template <typename T>
std::enable_if_t<std::is_integral_v<T>, T>
safe_divide(T a, T b) {
    if (b == 0) return 0;  // safe fallback for integers
    return a / b;
}

// Only enabled for floating-point types
template <typename T>
std::enable_if_t<std::is_floating_point_v<T>, T>
safe_divide(T a, T b) {
    if (b == 0.0) return std::numeric_limits<T>::quiet_NaN();
    return a / b;
}

// enable_if in template parameter (alternative placement)
template <typename T,
         std::enable_if_t<std::is_arithmetic_v<T>, int> = 0>
void process(T value) {
    std::cout << "Processing arithmetic value: " << value << '\n';
}

int main() {
    std::cout << safe_divide(10, 3)    << '\n'; // 3 (integer)
    std::cout << safe_divide(10.0, 3.0) << '\n'; // 3.33333 (double)
    std::cout << safe_divide(10, 0)    << '\n'; // 0 (safe fallback)
    std::cout << safe_divide(10.0, 0.0) << '\n'; // nan

    process(42);
    // process(std::string("hello")); // ERROR: no matching function
}

La bibliothèque <type_traits>

L'en-tête fournit une bibliothèque complète d'outils d'introspection et de transformation de types à la compilation. Ils constituent la colonne vertébrale de la programmation basée sur SFINAE.

Traits de requête (renvoient un booléen via ::value ou le suffixe _v) :
- std::is_integral_v — T est-il un type entier ?
- std::is_floating_point_v — T est-il un type à virgule flottante ?
- std::is_pointer_v — T est-il un pointeur ?
- std::is_class_v — T est-il une classe/struct ?
- std::is_const_v — T est-il qualifié const ?
- std::is_same_v — T et U sont-ils le même type ?
- std::is_base_of_v — Base est-elle une base de Derived ?
- std::is_convertible_v — From peut-il être converti implicitement en To ?
- std::is_constructible_v — T peut-il être construit à partir de Args ?

Traits de transformation (renvoient un type modifié via ::type ou le suffixe _t) :
- std::remove_reference_t — supprime & ou &&
- std::remove_const_t — supprime const
- std::decay_t — applique la décroissance tableau-vers-pointeur et fonction-vers-pointeur, et supprime les qualificatifs cv et les références
- std::conditional_t — ternaire à la compilation : si Cond alors T sinon F
- std::common_type_t — le type vers lequel tous les Ts peuvent être convertis

std::void_t et l'idiome de détection

std::void_t<...> (C++17) associe n'importe quelle séquence de types à void. Sa puissance vient de SFINAE : si l'un des types de la liste est invalide, la spécialisation est écartée. Cela permet l'idiome de détection — vérifier si un type possède un membre, une méthode ou un type imbriqué particulier.

void_t_detection.cpp
#include <iostream>
#include <type_traits>
#include <string>
#include <vector>

// Primary template: fallback, T does NOT have .size()
template <typename T, typename = void>
struct has_size : std::false_type {};

// Specialization: matches only if T has a .size() method
template <typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>>
    : std::true_type {};

template <typename T>
constexpr bool has_size_v = has_size<T>::value;

// Use the trait to branch behavior
template <typename T>
void describe(const T& value) {
    if constexpr (has_size_v<T>) {
        std::cout << "Container with " << value.size() << " elements\n";
    } else {
        std::cout << "Not a container: " << value << '\n';
    }
}

int main() {
    static_assert(has_size_v<std::string>);     // true
    static_assert(has_size_v<std::vector<int>>); // true
    static_assert(!has_size_v<int>);             // false

    describe(std::vector<int>{1, 2, 3}); // Container with 3 elements
    describe(42);                        // Not a container: 42
}

decltype avec SFINAE

Vous pouvez utiliser decltype dans les types de retour en fin de déclaration (trailing return types) pour déclencher SFINAE selon qu'une expression est valide ou non. Cette technique est souvent combinée à l'opérateur virgule pour ignorer le résultat de l'expression et renvoyer le type souhaité.

decltype_sfinae.cpp
#include <iostream>
#include <type_traits>

// Only callable if a + b is valid
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
    return a + b;
}

// Only callable if t.begin() and t.end() are valid
template <typename Container>
auto element_count(const Container& c)
    -> decltype(c.begin(), c.end(), std::size_t{}) {
    std::size_t count = 0;
    for (auto it = c.begin(); it != c.end(); ++it) ++count;
    return count;
}

int main() {
    std::cout << add(1, 2.5)   << '\n'; // 3.5
    std::cout << add(std::string("a"), std::string("b")) << '\n'; // ab

    std::vector<int> v{1, 2, 3, 4};
    std::cout << element_count(v) << '\n'; // 4
    // add(std::string("a"), 5); // SFINAE: no match, a+b invalid
}
Piège

Lorsque SFINAE élimine toutes les surcharges viables, le compilateur produit un mur de texte d'erreur listant chaque candidat essayé et la raison de chaque échec. Ces messages sont réputés pour être très difficiles à lire — ils s'étendent souvent sur des centaines de lignes pour une seule mauvaise utilisation.

Prenons un exemple : vous appelez safe_divide("hello", "world"). Le compilateur essaie les deux surcharges, les deux échouent par SFINAE, et vous obtenez un message d'erreur qui mentionne enable_if, is_integral, is_floating_point et les détails internes des type traits — rien qui vous aide à comprendre que le vrai problème est que « les chaînes ne sont pas des nombres ».

C'est l'une des plus fortes motivations des concepts de C++20, qui produisent des messages d'erreur clairs et lisibles, du type : « la contrainte std::integral n'est pas satisfaite pour T = const char* ».

Bonne pratique

Si vous écrivez du code C++20, préférez les concepts à SFINAE dans pratiquement tous les cas. Les concepts sont plus faciles à lire, se composent naturellement et produisent de bien meilleurs messages d'erreur. Réservez SFINAE pour :

- Le code qui doit compiler en C++11/14/17
- Les idiomes de détection complexes qui n'ont pas d'équivalent sous forme de concept standard
- Le code de bibliothèque qui doit prendre en charge plusieurs versions du standard C++

Même en C++17, préférez if constexpr avec des type traits à enable_if lorsque vous pouvez exprimer la logique au sein d'une seule fonction.

Points clés
  • SFINAE : si la substitution des arguments de template crée un type invalide dans le contexte immédiat, le template est retiré silencieusement de la résolution de surcharge
  • std::enable_if_t est l'outil SFINAE classique — il n'existe que lorsque Condition est vraie
  • fournit des traits de requête (is_integral_v) et des traits de transformation (remove_reference_t) à la compilation
  • std::void_t permet l'idiome de détection — vérifier si un type possède des membres ou des opérations précis
  • Les messages d'erreur de SFINAE sont notoirement illisibles — les concepts de C++20 en sont le remplaçant moderne

Quiz — Testez vos connaissances

(20 XP)

1. Que signifie SFINAE et qu'est-ce que cela implique ?

2. Qu'accomplit `std::void_t<decltype(std::declval<T>().size())>` dans une spécialisation de template ?

3. Que fait `std::decay_t<T>` ?