Přeskočit na obsah

SFINAE, Type Traits & enable_if

Understand the Substitution Failure Is Not An Error principle, use type traits for compile-time introspection, and constrain templates with enable_if.

What Is SFINAE?

SFINAE stands for Substitution Failure Is Not An Error. When the compiler tries to instantiate a template, it substitutes the deduced or specified types into the template. If this substitution produces an invalid type or expression in the immediate context of the template declaration, the compiler does not emit an error — it simply removes that template from the overload set and moves on to try other candidates.

This mechanism is the foundation of template metaprogramming in C++11/14. It allows you to write templates that are conditionally available based on properties of their type arguments.

The key phrase is "immediate context." Substitution failures deep inside a function body are hard errors, not SFINAE. Only failures in the function signature (return type, parameter types, template parameter list, or requires clause) trigger SFINAE.

std::enable_if

std::enable_ifdefinuje typ členatyperovnýTpouze v případě, žeConditionjetrue. Pokud jeConditionfalse, člentypeneexistuje, což způsobuje selhání substituce (SFINAE). Alias ​​std::enable_if_tje zkratka protypename 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
}

Knihovna <type_traits>

Hlavička poskytuje komplexní knihovnu nástrojů pro introspekci a transformaci typů v době kompilace. Tvoří páteř programování založeného na SFINAE.

Vlastnosti dotazu (vrátí bool přes::valuenebo příponu_v):
-std::is_integral_v— je T celočíselný typ?
-std::is_floating_point_v— je T typ s plovoucí desetinnou čárkou?
-std::is_pointer_v— je T ukazatel?
-std::is_class_v— je T třída/struktura?
-std::is_const_v– má T const kvalifikaci?
-std::is_same_v– jsou T a U stejného typu?
-std::is_base_of_v— je Base základem Derived?
-std::is_convertible_v– může From implicitně převést na To?
-std::is_constructible_v— lze T zkonstruovat z Args?

Transformační rysy (vrácení upraveného typu prostřednictvím::typenebo přípony_t):
-std::remove_reference_t— proužky&nebo&&
-std::remove_const_t— pásyconst
-std::decay_t– aplikuje útlum z pole na ukazatel, z funkce na ukazatel a odstraní cv/reference
-std::conditional_t— ternární v době kompilace: pokud Cond, pak T jinak F
-std::common_type_t— typ, na který lze převést všechny T

std::void_t & idiom detekce

std::void_t<...> (C++17) mapuje jakoukoli sekvenci typů navoid. Jeho síla spočívá ve SFINAE: pokud je některý typ v seznamu neplatný, specializace se zahodí. To umožňuje detekční idiom — kontrolu, zda má typ specifický člen, metodu nebo vnořený typ.

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 s SFINAE

V koncových návratových typech můžete použít decltype ke spuštění SFINAE na základě toho, zda je výraz platný. Tato technika se často kombinuje s operátorem čárka, aby se zahodil výsledek výrazu a vrátil se požadovaný typ.

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
}
Úskalí

Když SFINAE odstraní všechna životaschopná přetížení, kompilátor vytvoří blok chybového textu se seznamem všech kandidátů, které vyzkoušel, a proč každý selhal. Tyto zprávy jsou notoricky obtížné číst – často zahrnují stovky řádků pro jediné zneužití.

Zvažte toto: volátesafe_divide("hello", "world"). Kompilátor zkouší obě přetížení, obě selžou SFINAE a dostanete chybovou zprávu, která zmiňujeenable_if,is_integral,is_floating_pointa interní podrobnosti typových vlastností – nic z toho vám nepomůže pochopit, že skutečný problém je „řetězce nejsou čísla“.

Toto je jedna z nejsilnějších motivací pro koncepty C++20, které vytvářejí jasné a čitelné chybové zprávy jako: „omezenístd::integralnebylo proT = const char*splněno“.

Osvědčený postup

Pokud píšete kód C++20, upřednostňujte koncepty před SFINAE prakticky ve všech případech. Koncepty se snáze čtou, přirozeně se skládají a vytvářejí mnohem lepší chybové zprávy. Rezervujte SFINAE na:

- Kód, který se musí zkompilovat s C++11/14/17
- Komplexní detekční idiomy, které nemají standardní koncept ekvivalentu
- Kód knihovny, který musí podporovat více standardních verzí C++

I v kódu C++17 upřednostňujteif constexprs typovými vlastnostmi předenable_if, když můžete vyjádřit logiku v rámci jediné funkce.

Klíčové poznatky
  • SFINAE: pokud nahrazením argumentů šablony vytvoříte neplatný typ v okamžitém kontextu, šablona bude tiše odstraněna z řešení přetížení
  • std::enable_if_tje klasický nástroj SFINAE – existuje pouze tehdy, když je podmínka pravdivá
  • poskytuje vlastnosti dotazu při kompilaci (is_integral_v) a transformační vlastnosti (remove_reference_t)
  • std::void_tenables the detection idiom — checking if a type has specific members or operations
  • SFINAE error messages are notoriously unreadable — C++20 concepts are the modern replacement

Kvíz — Otestujte své znalosti

(20 XP)

1. What does SFINAE stand for and what does it mean?

2. What does`std::void_t<decltype(std::declval<T>().size())>`achieve in a template specialization?

3. What does`std::decay_t<T>`do?