Ricerca…


enable_if

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 .

void_t

C ++ 11

void_t è una meta-funzione che mappa qualsiasi (numero di) tipi di tipo void . Lo scopo principale di void_t è facilitare la scrittura di caratteri tipografici.

std::void_t sarà parte di C ++ 17, ma fino ad allora, è estremamente semplice implementare:

template <class...> using void_t = void;

Alcuni compilatori richiedono un'implementazione leggermente diversa:

template <class...>
struct make_void { using type = void; };

template <typename... T>
using void_t = typename make_void<T...>::type;

L'applicazione principale di void_t è la scrittura di tratti di tipo che controllano la validità di una dichiarazione. Ad esempio, controlliamo se un tipo ha una funzione membro foo() che non accetta argomenti:

template <class T, class=void>
struct has_foo : std::false_type {};

template <class T>
struct has_foo<T, void_t<decltype(std::declval<T&>().foo())>> : std::true_type {};

Come funziona? Quando provo a istanziare has_foo<T>::value , ciò farà sì che il compilatore cerchi di cercare la migliore specializzazione per has_foo<T, void> . Abbiamo due opzioni: la primaria, e questa secondaria che implica dover istanziare quella espressione sottostante:

  • Se T ha una funzione membro foo() , allora qualsiasi tipo che restituisce viene convertito in void , e la specializzazione è preferito al primario basati sul modello regole ordinamento parziale. Quindi has_foo<T>::value sarà true
  • Se T non ha una funzione membro (o richiede più di un argomento), la sostituzione non riesce per la specializzazione e abbiamo solo il modello principale su cui fare il fallback. Quindi, has_foo<T>::value è false .

Un caso più semplice:

template<class T, class=void>
struct can_reference : std::false_type {};

template<class T>
struct can_reference<T, std::void_t<T&>> : std::true_type {};

questo non usa std::declval o decltype .

Si può notare un modello comune di un argomento vuoto. Possiamo tenerne conto:

struct details {
  template<template<class...>class Z, class=void, class...Ts>
  struct can_apply:
    std::false_type
  {};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:
    std::true_type
  {};
};

template<template<class...>class Z, class...Ts>
using can_apply = details::can_apply<Z, void, Ts...>;

che nasconde l'uso di std::void_t e fa can_apply come un indicatore se il tipo fornito come primo argomento template è ben formato dopo aver sostituito gli altri tipi in esso. Gli esempi precedenti possono ora essere riscritti usando can_apply come:

template<class T>
using ref_t = T&;

template<class T>
using can_reference = can_apply<ref_t, T>;    // Is T& well formed for T?

e:

template<class T>
using dot_foo_r = decltype(std::declval<T&>().foo());

template<class T>
using can_dot_foo = can_apply< dot_foo_r, T >;    // Is T.foo() well formed for T?

che sembra più semplice delle versioni originali.

Ci sono post-C ++ 17 proposte di std tratti simili a can_apply .

L'utilità di void_t stata scoperta da Walter Brown. Ha tenuto una meravigliosa presentazione al CppCon 2016.

finale decltype nei modelli di funzione

C ++ 11

Una delle funzioni di limitazione consiste nell'utilizzare il decltype finale per specificare il tipo restituito:

namespace details {
   using std::to_string;

   // this one is constrained on being able to call to_string(T)
   template <class T>
   auto convert_to_string(T const& val, int )
       -> decltype(to_string(val))
   {
       return to_string(val);
   }

   // this one is unconstrained, but less preferred due to the ellipsis argument
   template <class T>
   std::string convert_to_string(T const& val, ... )
   {
       std::ostringstream oss;
       oss << val;
       return oss.str();
   }
}

template <class T>
std::string convert_to_string(T const& val)
{
    return details::convert_to_string(val, 0);
}

Se chiamo convert_to_string() con un argomento con cui posso invocare to_string() , allora ho due funzioni valide per details::convert_to_string() . Il primo è preferito poiché la conversione da 0 a int è una sequenza di conversione implicita migliore rispetto alla conversione da 0 a ...

Se chiamo convert_to_string() con un argomento dal quale non posso richiamare to_string() , allora la prima istanza del modello di funzione porta a un errore di sostituzione (non c'è decltype(to_string(val)) ). Di conseguenza, quel candidato viene rimosso dal set di sovraccarico. Il secondo modello di funzione non è vincolato, quindi è selezionato e passiamo invece attraverso l' operator<<(std::ostream&, T) . Se quello non è definito, allora abbiamo un errore di compilazione difficile con uno stack di template sulla linea oss << val .

Cos'è SFINAE

SFINAE sta per S ubstitution F ailure I s N OT A n e rror. Il codice malformato che deriva dal sostituire i tipi (o i valori) per creare un'istanza di un modello di funzione o un modello di classe non è un errore di compilazione difficile, viene considerato solo come un errore di deduzione.

I fallimenti di detrazioni su modelli di istanze di istanza o specializzazioni di modelli di classe rimuovono quel candidato dall'insieme di considerazioni - come se quel candidato fallito non esistesse per cominciare.

template <class T>
auto begin(T& c) -> decltype(c.begin()) { return c.begin(); }

template <class T, size_t N>
T* begin(T (&arr)[N]) { return arr; }

int vals[10];
begin(vals); // OK. The first function template substitution fails because
             // vals.begin() is ill-formed. This is not an error! That function
             // is just removed from consideration as a viable overload candidate,
             // leaving us with the array overload. 

Solo i guasti di sostituzione nel contesto immediato sono considerati errori di deduzione, tutti gli altri sono considerati errori gravi.

template <class T>
void add_one(T& val) { val += 1; }

int i = 4;
add_one(i); // ok

std::string msg = "Hello";
add_one(msg); // error. msg += 1 is ill-formed for std::string, but this
              // failure is NOT in the immediate context of substituting T

enable_if_all / enable_if_any

C ++ 11

Esempio motivazionale


Quando si dispone di un pacchetto modello variadic nell'elenco dei parametri del modello, come nel seguente frammento di codice:

template<typename ...Args> void func(Args &&...args) { //... };

La libreria standard (precedente a C ++ 17) non offre alcun modo diretto per scrivere enable_if per imporre vincoli SFINAE su tutti i parametri in Args o in qualsiasi parametro in Args . C ++ 17 offre std::conjunction e std::disjunction che risolvono questo problema. Per esempio:

/// C++17: SFINAE constraints on all of the parameters in Args.
template<typename ...Args,
         std::enable_if_t<std::conjunction_v<custom_conditions_v<Args>...>>* = nullptr>
void func(Args &&...args) { //... };

/// C++17: SFINAE constraints on any of the parameters in Args.
template<typename ...Args,
         std::enable_if_t<std::disjunction_v<custom_conditions_v<Args>...>>* = nullptr>
void func(Args &&...args) { //... };

Se non hai a disposizione C ++ 17, ci sono diverse soluzioni per raggiungerli. Uno di questi è usare una classe di casi base e specializzazioni parziali , come dimostrato nelle risposte a questa domanda .

In alternativa, si può anche implementare a mano il comportamento di std::conjunction e std::disjunction in modo piuttosto diretto. Nell'esempio seguente std::enable_if le implementazioni e le combineremo con std::enable_if per produrre due alias: enable_if_all e enable_if_any , che fanno esattamente ciò che dovrebbero semanticamente. Ciò potrebbe fornire una soluzione più scalabile.


Implementazione di enable_if_all e enable_if_any


Prima seq_and std::conjunction e std::disjunction usando rispettivamente seq_and e seq_or personalizzati:

/// Helper for prior to C++14.
template<bool B, class T, class F >
using conditional_t = typename std::conditional<B,T,F>::type;

/// Emulate C++17 std::conjunction.
template<bool...> struct seq_or: std::false_type {};
template<bool...> struct seq_and: std::true_type {};

template<bool B1, bool... Bs>
struct seq_or<B1,Bs...>: 
  conditional_t<B1,std::true_type,seq_or<Bs...>> {};

template<bool B1, bool... Bs>
struct seq_and<B1,Bs...>:
  conditional_t<B1,seq_and<Bs...>,std::false_type> {};  

Quindi l'implementazione è abbastanza semplice:

template<bool... Bs>
using enable_if_any = std::enable_if<seq_or<Bs...>::value>;

template<bool... Bs>
using enable_if_all = std::enable_if<seq_and<Bs...>::value>;

Alla fine alcuni aiutanti:

template<bool... Bs>
using enable_if_any_t = typename enable_if_any<Bs...>::type;

template<bool... Bs>
using enable_if_all_t = typename enable_if_all<Bs...>::type;

uso


Anche l'utilizzo è semplice:

    /// SFINAE constraints on all of the parameters in Args.
    template<typename ...Args,
             enable_if_all_t<custom_conditions_v<Args>...>* = nullptr>
    void func(Args &&...args) { //... };

    /// SFINAE constraints on any of the parameters in Args.
    template<typename ...Args,
             enable_if_any_t<custom_conditions_v<Args>...>* = nullptr>
    void func(Args &&...args) { //... };

is_detected

Per generalizzare la creazione di type_trait: basato su SFINAE ci sono tratti sperimentali detected_or , detected_t , is_detected .

Con i parametri del template typename Default , template <typename...> Op e typename ... Args :

  • is_detected : alias di std::true_type o std::false_type seconda della validità di Op<Args...>
  • detected_t : alias di Op<Args...> o nonesuch seconda della validità di Op<Args...> .
  • detected_or : alias di una struct con value_t che è is_detected e type che è Op<Args...> o Default dipendente dalla validità di Op<Args...>

che può essere implementato usando std::void_t per SFINAE come segue:

C ++ 17
namespace detail {
    template <class Default, class AlwaysVoid,
              template<class...> class Op, class... Args>
    struct detector
    {
        using value_t = std::false_type;
        using type = Default;
    };

    template <class Default, template<class...> class Op, class... Args>
    struct detector<Default, std::void_t<Op<Args...>>, Op, Args...>
    {
        using value_t = std::true_type;
        using type = Op<Args...>;
    };

} // namespace detail

// special type to indicate detection failure
struct nonesuch {
    nonesuch() = delete;
    ~nonesuch() = delete;
    nonesuch(nonesuch const&) = delete;
    void operator=(nonesuch const&) = delete;
};

template <template<class...> class Op, class... Args>
using is_detected =
    typename detail::detector<nonesuch, void, Op, Args...>::value_t;

template <template<class...> class Op, class... Args>
using detected_t = typename detail::detector<nonesuch, void, Op, Args...>::type;

template <class Default, template<class...> class Op, class... Args>
using detected_or = detail::detector<Default, void, Op, Args...>;

I tratti per rilevare la presenza del metodo possono quindi essere implementati semplicemente:

typename <typename T, typename ...Ts>
using foo_type = decltype(std::declval<T>().foo(std::declval<Ts>()...));

struct C1 {};

struct C2 {
    int foo(char) const;
};

template <typename T>
using has_foo_char = is_detected<foo_type, T, char>;

static_assert(!has_foo_char<C1>::value, "Unexpected");
static_assert(has_foo_char<C2>::value, "Unexpected");

static_assert(std::is_same<int, detected_t<foo_type, C2, char>>::value,
              "Unexpected");

static_assert(std::is_same<void, // Default
                           detected_or<void, foo_type, C1, char>>::value,
              "Unexpected");
static_assert(std::is_same<int, detected_or<void, foo_type, C2, char>>::value,
              "Unexpected");

Risoluzione di sovraccarico con un gran numero di opzioni

Se è necessario selezionare tra diverse opzioni, abilitare solo uno tramite enable_if<> può essere piuttosto ingombrante, poiché anche diverse condizioni devono essere negate.

L'ordine tra sovraccarichi può invece essere selezionato usando l'ereditarietà, cioè l'invio di tag.

Invece di testare la cosa che deve essere ben formata, e anche testare la negazione di tutte le altre condizioni delle versioni, decltype invece solo ciò di cui abbiamo bisogno, preferibilmente in un decltype in un ritorno finale.
Ciò potrebbe lasciare diverse opzioni ben formate, differenziamo tra quelle che usano i 'tag', simili ai tag iterator-trait ( random_access_tag et al). Funziona perché una corrispondenza diretta è migliore di una classe base, che è migliore di una classe base di una classe base, ecc.

#include <algorithm>
#include <iterator>

namespace detail
{
    // this gives us infinite types, that inherit from each other
    template<std::size_t N>
    struct pick : pick<N-1> {};
    template<>
    struct pick<0> {};

    // the overload we want to be preferred have a higher N in pick<N>
    // this is the first helper template function
    template<typename T>
    auto stable_sort(T& t, pick<2>)
        -> decltype( t.stable_sort(), void() )
    {
        // if the container have a member stable_sort, use that
        t.stable_sort();
    }

    // this helper will be second best match
    template<typename T>
    auto stable_sort(T& t, pick<1>)
        -> decltype( t.sort(), void() )
    {
        // if the container have a member sort, but no member stable_sort
        // it's customary that the sort member is stable
        t.sort();
    }

    // this helper will be picked last
    template<typename T>
    auto stable_sort(T& t, pick<0>)
        -> decltype( std::stable_sort(std::begin(t), std::end(t)), void() )
    {
        // the container have neither a member sort, nor member stable_sort
        std::stable_sort(std::begin(t), std::end(t));
    }

}

// this is the function the user calls. it will dispatch the call
// to the correct implementation with the help of 'tags'.
template<typename T>
void stable_sort(T& t)
{
    // use an N that is higher that any used above.
    // this will pick the highest overload that is well formed.
    detail::stable_sort(t, detail::pick<10>{});
}

Esistono altri metodi comunemente usati per distinguere tra sovraccarichi, ad esempio la corrispondenza esatta è migliore della conversione, essendo migliore dei puntini di sospensione.

Tuttavia, il tag-dispatch può estendersi a qualsiasi numero di scelte ed è un po 'più chiaro nell'intento.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow