SFINAE, type trait ed enable_if
Comprendi il principio Substitution Failure Is Not An Error, usa i type trait per l'introspezione a tempo di compilazione e vincola i template con enable_if.
Che cos'è SFINAE?
SFINAE sta per Substitution Failure Is Not An Error (un fallimento nella sostituzione non è un errore). Quando il compilatore prova a istanziare un template, sostituisce nel template i tipi dedotti o specificati. Se questa sostituzione produce un tipo o un'espressione non validi nel contesto immediato della dichiarazione del template, il compilatore non emette un errore: si limita a rimuovere quel template dall'insieme degli overload e passa a provare gli altri candidati.
Questo meccanismo è il fondamento della metaprogrammazione con i template in C++11/14. Ti permette di scrivere template disponibili condizionatamente in base alle proprietà dei loro argomenti di tipo.
L'espressione chiave è "contesto immediato". I fallimenti di sostituzione in profondità nel corpo di una funzione sono errori veri e propri, non SFINAE. Solo i fallimenti nella firma della funzione (tipo di ritorno, tipi dei parametri, lista dei parametri template o clausola requires) attivano SFINAE.
std::enable_if
std::enable_if definisce un tipo membro type uguale a T solo se Condition è true. Se Condition è false, il membro type non esiste e si produce un fallimento di sostituzione (SFINAE). L'alias std::enable_if_t è la forma abbreviata di typename std::enable_if.
#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 libreria <type_traits>
L'header offre una libreria completa di strumenti per l'introspezione e la trasformazione dei tipi a tempo di compilazione. Sono l'ossatura della programmazione basata su SFINAE.
Trait di interrogazione (restituiscono un bool tramite ::value o il suffisso _v):
- std::is_integral_v — T è un tipo intero?
- std::is_floating_point_v — T è un tipo in virgola mobile?
- std::is_pointer_v — T è un puntatore?
- std::is_class_v — T è una classe o una struct?
- std::is_const_v — T è qualificato const?
- std::is_same_v — T e U sono lo stesso tipo?
- std::is_base_of_v — Base è una classe base di Derived?
- std::is_convertible_v — From può convertirsi implicitamente in To?
- std::is_constructible_v — T può essere costruito a partire da Args?
Trait di trasformazione (restituiscono un tipo modificato tramite ::type o il suffisso _t):
- std::remove_reference_t — rimuove & o &&
- std::remove_const_t — rimuove const
- std::decay_t — applica il decadimento da array a puntatore e da funzione a puntatore, e rimuove cv e riferimento
- std::conditional_t — ternario a tempo di compilazione: se Cond allora T altrimenti F
- std::common_type_t — il tipo in cui tutti i Ts possono convertirsi
std::void_t e il detection idiom
std::void_t<...> (C++17) mappa qualunque sequenza di tipi su void. La sua potenza sta in SFINAE: se uno qualsiasi dei tipi della lista non è valido, la specializzazione viene scartata. Questo abilita il detection idiom — verificare se un tipo possiede un determinato membro, metodo o tipo annidato.
#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
Puoi usare decltype nei tipi di ritorno posticipati per attivare SFINAE in base alla validità di un'espressione. Questa tecnica viene spesso combinata con l'operatore virgola per scartare il risultato dell'espressione e restituire il tipo desiderato.
#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
}Quando SFINAE elimina tutti gli overload praticabili, il compilatore produce un muro di testo che elenca ogni candidato provato e il motivo di ciascun fallimento. Questi messaggi sono notoriamente difficili da leggere — spesso centinaia di righe per un singolo uso sbagliato.
Pensa a questo caso: chiami safe_divide("hello", "world"). Il compilatore prova entrambi gli overload, entrambi falliscono per SFINAE, e ottieni un messaggio di errore che cita enable_if, is_integral, is_floating_point e i dettagli interni dei type trait — nulla che ti aiuti a capire che il vero problema è che "le stringhe non sono numeri".
È una delle motivazioni più forti a favore dei concept di C++20, che producono messaggi di errore chiari e leggibili come: "il vincolo std::integral non è soddisfatto per T = const char*".
Se stai scrivendo codice C++20, preferisci i concept a SFINAE in praticamente tutti i casi. I concept sono più leggibili, si compongono in modo naturale e producono messaggi di errore molto migliori. Riserva SFINAE a:
- Codice che deve compilare con C++11/14/17
- Detection idiom complessi che non hanno un concept standard equivalente
- Codice di libreria che deve supportare più versioni dello standard C++
Anche nel codice C++17, preferisci if constexpr con i type trait a enable_if quando puoi esprimere la logica all'interno di una singola funzione.
- SFINAE: se la sostituzione degli argomenti template crea un tipo non valido nel contesto immediato, il template viene rimosso in silenzio dalla risoluzione degli overload
std::enable_if_tè lo strumento SFINAE classico — esiste solo quando Condition è verafornisce trait di interrogazione (is_integral_v) e trait di trasformazione (remove_reference_t) a tempo di compilazionestd::void_tabilita il detection idiom — verificare se un tipo ha membri o operazioni specifiche- I messaggi di errore di SFINAE sono notoriamente illeggibili — i concept di C++20 sono il sostituto moderno
Quiz — Metti alla prova le tue conoscenze
(20 XP)1. Cosa significa l'acronimo SFINAE e qual è il suo senso?
2. Che effetto ha `std::void_t<decltype(std::declval<T>().size())>` in una specializzazione di template?
3. Cosa fa `std::decay_t<T>`?