Perché dovrei evitare std :: enable_if nelle firme delle funzioni


165

Scott Meyers ha pubblicato il contenuto e lo stato del suo prossimo libro EC ++ 11. Ha scritto che un articolo del libro potrebbe essere "Evita le std::enable_iffirme delle funzioni" .

std::enable_if può essere utilizzato come argomento di funzione, come tipo restituito o come modello di classe o parametro di modello di funzione per rimuovere condizionalmente funzioni o classi dalla risoluzione di sovraccarico.

In questa domanda sono mostrate tutte e tre le soluzioni.

Come parametro di funzione:

template<typename T>
struct Check1
{
   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, int>::value >::type* = 0) { return 42; }

   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, double>::value >::type* = 0) { return 3.14; }   
};

Come parametro del modello:

template<typename T>
struct Check2
{
   template<typename U = T, typename std::enable_if<
            std::is_same<U, int>::value, int>::type = 0>
   U read() { return 42; }

   template<typename U = T, typename std::enable_if<
            std::is_same<U, double>::value, int>::type = 0>
   U read() { return 3.14; }   
};

Come tipo di ritorno:

template<typename T>
struct Check3
{
   template<typename U = T>
   typename std::enable_if<std::is_same<U, int>::value, U>::type read() {
      return 42;
   }

   template<typename U = T>
   typename std::enable_if<std::is_same<U, double>::value, U>::type read() {
      return 3.14;
   }   
};
  • Quale soluzione dovrebbe essere preferita e perché dovrei evitare gli altri?
  • In quali casi "Evita std::enable_ifnelle firme di funzione" riguarda l'utilizzo come tipo di restituzione (che non fa parte della normale firma della funzione ma delle specializzazioni del modello)?
  • Ci sono differenze per i modelli di funzioni membri e non membri?

Perché il sovraccarico è altrettanto bello, di solito. Se non altro, delegare a un'implementazione che utilizza modelli di classe (specializzati).
sehe

Le funzioni membro differiscono in quanto il set di sovraccarico include sovraccarichi dichiarati dopo il sovraccarico corrente. Ciò è particolarmente importante quando si eseguono tipi di ritorno ritardati variabili (dove il tipo di ritorno deve essere dedotto da un altro sovraccarico)
vedere il

1
Bene, solo soggettivamente devo dire che, sebbene spesso sia abbastanza utile, non mi piace std::enable_ifingombrare le mie firme di funzione (specialmente la brutta nullptrversione dell'argomento della funzione aggiuntiva ) perché sembra sempre quello che è, uno strano hack (per qualcosa un static ifpotere fare molto più bello e pulito) usando il template black-magic per sfruttare una caratteristica del linguaggio interessante. Questo è il motivo per cui preferisco il tag-dispatching quando possibile (beh, hai ancora argomenti strani aggiuntivi, ma non nell'interfaccia pubblica e anche molto meno brutta e criptica ).
Christian Rau,

2
Voglio chiedere ciò che =0a typename std::enable_if<std::is_same<U, int>::value, int>::type = 0realizzare? Non sono riuscito a trovare le risorse corrette per capirlo. So che la prima parte prima =0ha un tipo di membro intse Ued intè la stessa. Grazie molto!
astroboylrx,

4
@astroboylrx Divertente, stavo solo per aggiungere un commento rilevando questo. Fondamentalmente, quello = 0 indica che si tratta di un parametro modello predefinito non di tipo . È fatto in questo modo perché i parametri del modello di tipo predefinito non fanno parte della firma, quindi non è possibile sovraccaricarli.
Nir Friedman,

Risposte:


107

Inserisci l'hack nei parametri del modello .

L' enable_ifapproccio con i parametri on template presenta almeno due vantaggi rispetto agli altri:

  • leggibilità : l'uso di enable_if e i tipi return / argomento non vengono uniti in un blocco disordinato di disambiguatori di nomi di tipo e accessi di tipo nidificati; anche se il disordine del disambiguatore e del tipo nidificato può essere mitigato con modelli di alias, che unirebbero comunque due cose non correlate. L'utilizzo di enable_if è correlato ai parametri del modello e non ai tipi restituiti. Avendoli nei parametri del modello significa che sono più vicini a ciò che conta;

  • applicabilità universale : i costruttori non hanno tipi di restituzione e alcuni operatori non possono avere argomenti aggiuntivi, quindi nessuna delle altre due opzioni può essere applicata ovunque. Mettere enable_if in un parametro template funziona ovunque poiché puoi usare SFINAE solo sui template.

Per me, l'aspetto della leggibilità è il grande fattore motivante in questa scelta.


4
L'uso della FUNCTION_REQUIRESmacro qui rende molto più piacevole la lettura e funziona anche con i compilatori C ++ 03 e si basa sull'utilizzo enable_ifnel tipo restituito. Inoltre, l'utilizzo dei enable_ifparametri del modello di funzione causa problemi di sovraccarico, poiché ora la firma della funzione non è univoca e causa errori di sovraccarico ambigui.
Paul Fultz II

3
Questa è una vecchia domanda, ma per chiunque continui a leggere: la soluzione al problema sollevato da @Paul è quella di utilizzare enable_ifun parametro di modello non di tipo predefinito, che consente il sovraccarico. Cioè enable_if_t<condition, int> = 0invece di typename = enable_if_t<condition>.
Nir Friedman,


@ R.MartinhoFernandes il flamingdangerzonelink nel tuo commento sembra portare ora a una pagina di installazione di spyware. L'ho segnalato per l'attenzione del moderatore.
nispio,

58

std::enable_ifsi basa sul principio " Errore di sostituzione non è un errore " (aka SFINAE) durante la deduzione dell'argomento modello . Questa è una funzione linguistica molto fragile e devi stare molto attento a farlo bene.

  1. se la tua condizione all'interno enable_ifcontiene un modello nidificato o una definizione di tipo (suggerimento: cerca ::token), la risoluzione di questi templi o tipi nidificati è di solito un contesto non dedotto . Qualsiasi errore di sostituzione in un tale contesto non dedotto è un errore .
  2. le varie condizioni in enable_ifsovraccarichi multipli non possono sovrapporsi poiché la risoluzione del sovraccarico sarebbe ambigua. Questo è qualcosa che tu come autore devi controllare, anche se otterrai buoni avvisi per il compilatore.
  3. enable_ifmanipola l'insieme di funzioni vitali durante la risoluzione di sovraccarico che può avere interazioni sorprendenti a seconda della presenza di altre funzioni che vengono introdotte da altri ambiti (ad esempio tramite ADL). Questo non lo rende molto robusto.

In breve, quando funziona funziona, ma in caso contrario può essere molto difficile eseguire il debug. Un'ottima alternativa è usare il dispacciamento di tag , cioè delegare a una funzione di implementazione (di solito in uno detailspazio dei nomi o in una classe helper) che riceve un argomento fittizio basato sulla stessa condizione di compilazione che si usa in enable_if.

template<typename T>
T fun(T arg) 
{ 
    return detail::fun(arg, typename some_template_trait<T>::type() ); 
}

namespace detail {
    template<typename T>
    fun(T arg, std::false_type /* dummy */) { }

    template<typename T>
    fun(T arg, std::true_type /* dummy */) {}
}

Il dispacciamento dei tag non manipola il set di sovraccarico, ma aiuta a selezionare esattamente la funzione desiderata fornendo gli argomenti corretti attraverso un'espressione in fase di compilazione (ad es. In un tratto di tipo). Nella mia esperienza, è molto più facile eseguire il debug e ottenere il risultato corretto. Se sei un aspirante scrittore di librerie con tratti di tipo sofisticato, potresti aver bisogno in enable_ifqualche modo, ma per l'uso più regolare delle condizioni in fase di compilazione non è raccomandato.


22
Il dispacciamento di tag presenta tuttavia uno svantaggio: se si dispone di qualche tratto che rileva la presenza di una funzione e tale funzione è implementata con l'approccio di dispacciamento di tag, segnala sempre quel membro come presente e si traduce in un errore invece di un potenziale errore di sostituzione . SFINAE è principalmente una tecnica per rimuovere i sovraccarichi dai set candidati e il dispacciamento dei tag è una tecnica per selezionare tra due (o più) sovraccarichi. C'è qualche sovrapposizione nella funzionalità, ma non sono equivalenti.
R. Martinho Fernandes,

@ R.MartinhoFernandes puoi fare un breve esempio e illustrare come enable_iffarebbe?
TemplateRex

1
@ R.MartinhoFernandes Penso che una risposta separata che spieghi questi punti potrebbe aggiungere valore all'OP. :-) A proposito, scrivere tratti come is_f_ableè qualcosa che considero un compito per gli scrittori di biblioteche che ovviamente possono usare SFINAE quando questo dà loro un vantaggio, ma per gli utenti "normali" e dato un tratto is_f_able, penso che il dispacciamento dei tag sia più facile.
TemplateRex

1
@hansmaad Ho pubblicato una breve risposta indirizzata alla tua domanda e affronterò il problema di "a SFINAE o non a SFINAE" in un post sul blog (è un po 'fuori tema su questa domanda). Non appena avrò il tempo di finirlo, intendo.
R. Martinho Fernandes,

8
SFINAE è "fragile"? Che cosa?
Corse di leggerezza in orbita

5

Quale soluzione dovrebbe essere preferita e perché dovrei evitare gli altri?

  • Il parametro template

    • È utilizzabile nei costruttori.
    • È utilizzabile nell'operatore di conversione definito dall'utente.
    • Richiede C ++ 11 o successivo.
    • È l'IMO, il più leggibile.
    • Potrebbe essere facilmente utilizzato in modo errato e produce errori con sovraccarichi:

      template<typename T, typename = std::enable_if_t<std::is_same<T, int>::value>>
      void f() {/*...*/}
      
      template<typename T, typename = std::enable_if_t<std::is_same<T, float>::value>>
      void f() {/*...*/} // Redefinition: both are just template<typename, typename> f()

    Avviso typename = std::enable_if_t<cond>invece di correttostd::enable_if_t<cond, int>::type = 0

  • tipo di ritorno:

    • Non può essere utilizzato nel costruttore. (nessun tipo di reso)
    • Non può essere utilizzato nell'operatore di conversione definito dall'utente. (non deducibile)
    • Può essere utilizzato pre-C ++ 11.
    • Secondo IMO più leggibile.
  • Infine, nel parametro funzione:

    • Può essere utilizzato pre-C ++ 11.
    • È utilizzabile nei costruttori.
    • Non può essere utilizzato nell'operatore di conversione definito dall'utente. (nessun parametro)
    • Non può essere utilizzato nei metodi con numero fisso di argomenti (/ operatori binari unari +, -, *, ...)
    • Può essere tranquillamente utilizzato in eredità (vedi sotto).
    • Cambia la firma della funzione (hai sostanzialmente un extra come ultimo argomento void* = nullptr) (quindi il puntatore alla funzione differirebbe e così via)

Ci sono differenze per i modelli di funzioni membri e non membri?

Ci sono sottili differenze con l'ereditarietà e using:

Secondo il using-declarator(corsivo mio):

namespace.udecl

La serie di dichiarazioni introdotte dal using-dichiarator viene trovata eseguendo la ricerca del nome qualificata ([basic.lookup.qual], [class.member.lookup]) per il nome nel using-dichiarator, escluse le funzioni nascoste come descritto sotto.

...

Quando un dichiarante che utilizza porta dichiarazioni da una classe base in una classe derivata, le funzioni membro e i modelli di funzione membro nella classe derivata sovrascrivono e / o nascondono le funzioni membro e i modelli funzione membro con lo stesso nome, parametro-tipo-elenco, cv- qualifica e qualificatore di riferimento (se presente) in una classe base (anziché in conflitto). Tali dichiarazioni nascoste o sovrascritte sono escluse dall'insieme delle dichiarazioni introdotte dal dichiarante utilizzatore.

Quindi, sia per l'argomento modello che per il tipo restituito, i metodi sono nascosti nel seguente scenario:

struct Base
{
    template <std::size_t I, std::enable_if_t<I == 0>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 0> g() {}
};

struct S : Base
{
    using Base::f; // Useless, f<0> is still hidden
    using Base::g; // Useless, g<0> is still hidden

    template <std::size_t I, std::enable_if_t<I == 1>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 1> g() {}
};

Demo (gcc trova erroneamente la funzione base).

Considerando che con argomenti, uno scenario simile funziona:

struct Base
{
    template <std::size_t I>
    void h(std::enable_if_t<I == 0>* = nullptr) {}
};

struct S : Base
{
    using Base::h; // Base::h<0> is visible

    template <std::size_t I>
    void h(std::enable_if_t<I == 1>* = nullptr) {}
};

dimostrazione

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.