Ir al contenido

SFINAE, Type Traits y enable_if

Comprende el principio de Substitution Failure Is Not An Error, usa type traits para introspeccion en tiempo de compilacion, y restringe templates con enable_if.

Que Es SFINAE?

SFINAE significa Substitution Failure Is Not An Error (El Fallo de Sustitucion No Es Un Error). Cuando el compilador intenta instanciar un template, sustituye los tipos deducidos o especificados en el template. Si esta sustitucion produce un tipo o expresion invalida en el contexto inmediato de la declaracion del template, el compilador no emite un error — simplemente elimina ese template del conjunto de sobrecarga y pasa a probar otros candidatos.

Este mecanismo es la base de la metaprogramacion de templates en C++11/14. Te permite escribir templates que estan condicionalmente disponibles basandose en propiedades de sus argumentos de tipo.

La frase clave es "contexto inmediato." Los fallos de sustitucion profundos dentro del cuerpo de una funcion son errores duros, no SFINAE. Solo los fallos en la firma de la funcion (tipo de retorno, tipos de parametros, lista de parametros de template, o clausula requires) activan SFINAE.

std::enable_if

std::enable_if define un tipo miembro type igual a T solo si Condition es true. Si Condition es false, el miembro type no existe, causando un fallo de sustitucion (SFINAE). El alias std::enable_if_t es una abreviatura de 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 Biblioteca <type_traits>

El header proporciona una biblioteca completa de herramientas de introspeccion y transformacion de tipos en tiempo de compilacion. Forman la columna vertebral de la programacion basada en SFINAE.

Query traits (devuelven un bool mediante ::value o el sufijo _v):
- std::is_integral_v — es T un tipo entero?
- std::is_floating_point_v — es T un tipo de punto flotante?
- std::is_pointer_v — es T un puntero?
- std::is_class_v — es T una clase/struct?
- std::is_const_v — es T calificado como const?
- std::is_same_v — son T y U el mismo tipo?
- std::is_base_of_v — es Base una base de Derived?
- std::is_convertible_v — puede From convertirse implicitamente a To?
- std::is_constructible_v — puede T construirse a partir de Args?

Transformation traits (devuelven un tipo modificado mediante ::type o el sufijo _t):
- std::remove_reference_t — elimina & o &&
- std::remove_const_t — elimina const
- std::decay_t — aplica decaimiento de array a puntero, de funcion a puntero, y elimina cv/referencia
- std::conditional_t — ternario en tiempo de compilacion: si Cond entonces T sino F
- std::common_type_t — el tipo al que todos los Ts pueden convertirse

std::void_t y el Patron de Deteccion

std::void_t<...> (C++17) mapea cualquier secuencia de tipos a void. Su poder radica en SFINAE: si algun tipo en la lista es invalido, la especializacion se descarta. Esto habilita el patron de deteccion (detection idiom) — verificar si un tipo tiene un miembro, metodo o tipo anidado especifico.

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

Puedes usar decltype en tipos de retorno trailing para activar SFINAE basandote en si una expresion es valida. Esta tecnica se combina a menudo con el operador de coma para descartar el resultado de la expresion y devolver el tipo deseado.

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
}
Error común

Cuando SFINAE elimina todas las sobrecargas viables, el compilador produce un muro de texto de error listando cada candidato que probo y por que cada uno fallo. Estos mensajes son notoriamente dificiles de leer — a menudo abarcan cientos de lineas por un solo uso incorrecto.

Considera esto: llamas a safe_divide("hello", "world"). El compilador prueba ambas sobrecargas, ambas fallan en SFINAE, y obtienes un mensaje de error que menciona enable_if, is_integral, is_floating_point, y los detalles internos de type traits — nada de lo cual te ayuda a entender que el problema real es "los strings no son numeros."

Esta es una de las motivaciones mas fuertes para los concepts de C++20, que producen mensajes de error claros y legibles como: "the constraint std::integral was not satisfied for T = const char*".

Buena práctica

Si estas escribiendo codigo C++20, prefiere concepts sobre SFINAE en practicamente todos los casos. Los concepts son mas faciles de leer, se componen naturalmente, y producen mensajes de error mucho mejores. Reserva SFINAE para:

- Codigo que debe compilar con C++11/14/17
- Patrones de deteccion complejos que no tienen un concept estandar equivalente
- Codigo de biblioteca que debe soportar multiples versiones del estandar de C++

Incluso en codigo C++17, prefiere if constexpr con type traits sobre enable_if cuando puedes expresar la logica dentro de una sola funcion.

Puntos clave
  • SFINAE: si sustituir argumentos de template crea un tipo invalido en el contexto inmediato, el template se elimina silenciosamente de la resolucion de sobrecarga
  • std::enable_if_t es la herramienta clasica de SFINAE — solo existe cuando Condition es true
  • proporciona query traits en tiempo de compilacion (is_integral_v) y transformation traits (remove_reference_t)
  • std::void_t habilita el patron de deteccion — verificar si un tipo tiene miembros u operaciones especificas
  • Los mensajes de error de SFINAE son notoriamente ilegibles — los concepts de C++20 son el reemplazo moderno

Quiz — Pon a prueba tus conocimientos

(20 XP)

1. Que significa SFINAE y que implica?

2. Que logra `std::void_t<decltype(std::declval<T>().size())>` en una especializacion de template?

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