C++ enable_if


Esempio

std::enable_if è una comoda utilità per utilizzare le condizioni booleane per attivare SFINAE. È definito come:

template <bool Cond, typename Result=void>
struct enable_if { };

template <typename Result>
struct enable_if<true, Result> {
    using type = Result;
};

Cioè, enable_if<true, R>::type è un alias per R , mentre enable_if<false, T>::type è mal formato poiché quella specializzazione di enable_if non ha un type membro di tipo.

std::enable_if può essere usato per vincolare i template:

int negate(int i) { return -i; }

template <class F>
auto negate(F f) { return -f(); }

Qui, una chiamata a negate(1) fallirebbe a causa dell'ambiguità. Ma il secondo sovraccarico non è destinato a essere utilizzato per i tipi interi, quindi possiamo aggiungere:

int negate(int i) { return -i; }

template <class F, class = typename std::enable_if<!std::is_arithmetic<F>::value>::type>
auto negate(F f) { return -f(); }

Ora, l'istanziazione di negate<int> comporterebbe un errore di sostituzione poiché !std::is_arithmetic<int>::value è false . A causa di SFINAE, questo non è un errore grave, questo candidato viene semplicemente rimosso dal set di sovraccarico. Di conseguenza, negate(1) ha un solo candidato valido, che viene poi chiamato.

Quando usarlo

Vale la pena ricordare che std::enable_if è un aiuto su SFINAE, ma non è ciò che fa funzionare SFINAE in primo luogo. Consideriamo queste due alternative per implementare funzionalità simili a std::size , ovvero una size(arg) set di overload size(arg) che produce la dimensione di un contenitore o array:

// for containers
template<typename Cont>
auto size1(Cont const& cont) -> decltype( cont.size() );

// for arrays
template<typename Elt, std::size_t Size>
std::size_t size1(Elt const(&arr)[Size]);

// implementation omitted
template<typename Cont>
struct is_sizeable;

// for containers
template<typename Cont, std::enable_if_t<std::is_sizeable<Cont>::value, int> = 0>
auto size2(Cont const& cont);

// for arrays
template<typename Elt, std::size_t Size>
std::size_t size2(Elt const(&arr)[Size]);

Supponendo che is_sizeable sia scritto appropriatamente, queste due dichiarazioni dovrebbero essere esattamente equivalenti rispetto a SFINAE. Qual è il modo più semplice per scrivere, e quale è il modo più semplice da rivedere e capire a colpo d'occhio?

Consideriamo ora come potremmo voler implementare help aritmetici che evitino l'overflow dei caratteri interi in favore di un comportamento wrap-around o modulare. Il che incr(i, 3) ad es. incr(i, 3) sarebbe lo stesso di i += 3 salvo il fatto che il risultato sarebbe sempre definito anche se i sono un int con valore INT_MAX . Queste sono due alternative possibili:

// handle signed types
template<typename Int>
auto incr1(Int& target, Int amount)
-> std::void_t<int[static_cast<Int>(-1) < static_cast<Int>(0)]>;

// handle unsigned types by just doing target += amount
// since unsigned arithmetic already behaves as intended
template<typename Int>
auto incr1(Int& target, Int amount)
-> std::void_t<int[static_cast<Int>(0) < static_cast<Int>(-1)]>;
 
template<typename Int, std::enable_if_t<std::is_signed<Int>::value, int> = 0>
void incr2(Int& target, Int amount);
 
template<typename Int, std::enable_if_t<std::is_unsigned<Int>::value, int> = 0>
void incr2(Int& target, Int amount);

Ancora una volta qual è il modo più semplice per scrivere, e quale è il modo più semplice da rivedere e capire a colpo d'occhio?

Un punto di forza di std::enable_if è il modo in cui gioca con il refactoring e la progettazione dell'API. Se is_sizeable<Cont>::value è pensato per riflettere se cont.size() è valido, basta usare l'espressione così come appare per size1 può essere più conciso, anche se ciò potrebbe dipendere dal fatto che is_sizeable possa essere usato in più posti o meno . Contrasto a quello con std::is_signed che riflette la sua intenzione molto più chiaramente di quando la sua implementazione incr1 nella dichiarazione di incr1 .