Perché dovrei evitare di std::enable_if in funzione di firme

Scott Meyers inviato il contenuto e lo stato del suo prossimo libro CE++11.
Ha scritto che è un elemento del libro potrebbe essere “Evitare std::enable_if in funzione di firme”.

std::enable_if può essere usato come un argomento di una funzione, come tipo di ritorno o come un modello di classe o funzione di parametro del modello in modo condizionale rimuovere funzioni o classi da sovraccarico di risoluzione.

In questa domanda tutte e tre le soluzioni sono mostrati.

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 di 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 sia la soluzione dovrebbe essere preferito e perché dovrei evitare altri?
  • In quali casi “Evitare std::enable_if in funzione di firme” riguarda l’utilizzo come tipo di ritorno (che non è parte della normale funzione di firma, ma di modello di specializzazioni)?
  • Ci sono differenze per i membri e non membri in funzione dei modelli?
  • Perché il sovraccarico è altrettanto bello, di solito. Se non altro, delegato per un’implementazione che usa (specialized) modelli di classe.
  • Le funzioni membro la differenza è che il sovraccarico set include sovraccarichi dichiarato dopo il sovraccarico di corrente. Questo è particolarmente importante quando si fa variadics ritardato tipo di ritorno (in cui il tipo di ritorno è essere dedotte da un altro sovraccarico)
  • Beh, solo soggettivamente devo dire che pur essendo spesso molto utile, non mi piace std::enable_if di ingombrare la mia funzione di firme (in particolare il brutto ulteriori nullptr argomento della funzione della versione), perché sembra sempre che cosa è, una strana hack (per qualcosa di un static if potrebbe fare molto di più bella e pulita) utilizzando il modello di black-magic per sfruttare un interresting funzionalità del linguaggio. Questo è il motivo per cui preferisco tag-spedizione ogni volta che è possibile (beh, si hanno ancora ulteriori strano argomenti, ma non nell’interfaccia pubblica e anche molto meno brutto e criptico).
  • Vorrei chiedere che cosa fa =0 in typename std::enable_if<std::is_same<U, int>::value, int>::type = 0 realizzare? Non riuscivo a trovare la giusta risorse per capirlo. So che la prima parte prima di =0 è un tipo di membro int se U e int è lo stesso. Molte grazie!
  • Divertente, stavo solo andando a mettere un commento notare questo. Fondamentalmente, che =0 indica che questa è una di default, non il tipo di parametro del modello. E ‘ fatto in questo modo perché di default il tipo di modello parametri non sono parte della firma, quindi non è possibile sovraccarico su di loro.
InformationsquelleAutor hansmaad | 2013-01-30



3 Replies
  1. 103

    Mettere l’hack nel modello parametri.

    Il enable_if sul parametro di modello di approccio ha almeno due vantaggi rispetto agli altri:

    • leggibilità: il enable_if uso e ritorno/tipi di argomento non sono unite insieme in un sudicio pezzo di typename disambiguators e tipo annidato accessi; anche se l’ingombro del disambiguator e tipo annidato può essere mitigato con l’alias di modelli, che sarebbe ancora unire due cose non correlate insieme. Il enable_if uso è legato per i parametri del modello non i tipi di ritorno. Avendo loro i parametri del modello significa che sono più vicini a ciò che conta;

    • universale applicabilità: i costruttori non hanno il ritorno tipi, e alcuni operatori non hanno più argomenti, in modo che nessuna delle altre due opzioni possono essere applicate in tutto il mondo. Mettere enable_if in un parametro di modello funziona ovunque dal momento che è possibile utilizzare solo SFINAE su modelli comunque.

    Per me, la leggibilità aspetto è il grande fattore motivante in questa scelta.

    • È inoltre possibile rendere ancora più leggibile con alias modelli e alcuni tuning: flamingdangerzone.com/cxx11/2012/06/01/almost-static-if.html
    • Utilizzando il FUNCTION_REQUIRES macro qui, lo rende molto più piacevole da leggere, e funziona in C++03 compilatori, e si basa sull’utilizzo di enable_if il tipo di ritorno. Inoltre, l’utilizzo di enable_if in funzione di parametri del modello a causa di problemi di sovraccarico, perché ora la funzione di firma non sono univoci causando ambiguo sovraccarico errori.
    • Non broken link: flamingdangerzone.com/cxx11/almost-static-if
    • oh, grazie.
    • Questa è una vecchia questione, ma per tutti coloro che continuano a leggere: la soluzione per il problema sollevato da @Paolo è quello di utilizzare enable_if con un default non-tipo di parametro del modello, che consente di sovraccarico. I. e. enable_if_t<condition, int> = 0 invece di typename = enable_if_t<condition>.
    • wayback link al quasi-statico-se: web.archive.org/web/20150726012736/http://flamingdangerzone.com/…

  2. 55

    std::enable_if si basa sul “Sostituzione il Fallimento Non È Un Errore” (aka SFINAE) principio durante modello argomento detrazione. Questo è un molto fragile linguaggio e avete bisogno di essere molto attenti a farlo bene.

    1. se la tua condizione all’interno della enable_if contiene un modello nidificato o tipo di definizione (suggerimento: cercare :: token), quindi la risoluzione di questi nidificati tempatles o tipi sono di solito un non dedotta contesto. Qualsiasi sostituzione di guasto su un non-dedotto il contesto è un’ errore.
    2. le diverse condizioni di impiego più enable_if sovraccarichi non hanno alcuna sovrapposizione, perché la risoluzione dell’overload sarebbe ambiguo. Questo è qualcosa che è come un autore ha bisogno di controllare voi stessi, anche se si potrebbe ottenere un buon avvisi del compilatore.
    3. enable_if manipola l’insieme delle funzioni vitali durante la risoluzione di sovraccarico che può avere sorprendenti interazioni a seconda della presenza di altre funzioni che sono portati da altri ambiti (per esempio, attraverso l’ADL). Questo lo rende non molto robusto.

    In breve, quando funziona, funziona, ma se non può essere molto difficile per eseguire il debug. Una buona alternativa è quella di utilizzare tag dispacciamento, ovvero delegare ad una funzione di implementazione (di solito in un detail spazio dei nomi o in una classe di supporto) che riceve un argomento fittizio, basato sulla stessa fase di compilazione condizione che si utilizza 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 */) {}
    }

    Tag di spedizione non modificare il sovraccarico set, ma consente di selezionare esattamente la funzione che si desidera, fornendo gli opportuni argomenti attraverso un tempo di compilazione di espressione (ad esempio, in un tipo di carattere). Nella mia esperienza, questo è molto più facile per eseguire il debug e ottenere il diritto. Se sei un aspirante biblioteca scrittore di tipo sofisticato tratti, potrebbe essere necessario enable_if in qualche modo, ma per la maggior parte l’uso regolare di compilazione condizioni non è raccomandato.

    • Tag dispacciamento ha uno svantaggio: se avete qualche tratto, che rileva la presenza di una funzione, e che questa funzione è implementata con il tag dispacciamento tipo di approccio, è sempre la relazione che gli stati, come il presente, e come risultato un errore invece di una sostituzione potenziale fallimento. SFINAE è principalmente una tecnica per la rimozione di sovraccarichi candidati di imposta, e tag di dispacciamento è una tecnica per la scelta tra due (o più) dei sovraccarichi. Vi è una certa sovrapposizione in termini di funzionalità, ma non sono equivalenti.
    • si può dare un breve esempio, e illustrare come enable_if avrebbe capito giusto?
    • Ho aggiornato la mia risposta per riflettere il tuo commento un po’, tnx.
    • Breve? Mi dispiace 🙁 Immaginate di implementare una funzione f come modello per tutti i tipi di che soddisfare alcune condizioni e non anche per altri tipi. Se si utilizza il tag di spedizione, non sono un false_type sovraccarico. Va bene, causa un errore quando si tenta di utilizzare quel op>> per il male che tipi. Non hai nemmeno bisogno di un tag di spedizione per questo, si potrebbe utilizzare il static_assert e dare il meglio di messaggi di errore. (continua)
    • Ma consideriamo ora un tratto is_f_able che ti dice se l’espressione f(std::declval<T>()) è valido. Quel tratto sarà probabilmente utilizzare una qualche forma decltype su tale espressione per rilevare questo (il solito “membro” tipo di carattere). Il problema è, decltype(f(std::declval<T>())) sarà “lavoro” (cioè non mancherà di sostituzione) o produrre una statico errore di asserzione. Nessuna di queste opzioni è auspicabile, in quanto significa che non si ha mai una ::value falsa in quel tratto. SFINAE rende la funzione non, e il tratto rileva che correttamente per produrre un falso.
    • Spero che sia chiaro :/ sono in grado di scrivere questo scenario come un esempio di codice se non è chiaro.
    • Penso che una risposta separato spiegare questi punti potrebbero aggiungere valore per l’OP. 🙂 BTW, la scrittura tratti come is_f_able è qualcosa che io considero un compito per la biblioteca di scrittori che si possono naturalmente utilizzare SFINAE quando che dà loro un vantaggio, ma per gli utenti “normali” e dato un tratto is_f_able, penso tag dispacciamento è più facile.
    • Buon punto! Il fatto che essa non ha un singolo commento mi hanno ricordato che 😉 lo farò.
    • Come un esempio concreto di questo problema, operator< su un std::vector come contenitore — che potrebbe esistere o non basata su SFINAE di T <, o potrebbe esistere e funzionare solo se < opereT. Ho avuto a specializzarsi mio possono essere ordinati ” modello per la vector fare SFINAE perché vector non.
    • Sto ancora scrivendo la mia risposta, e che mi fa pensare più a fondo e con attenzione sui problemi: non sono sicuro che SFINAE è in gran parte per la biblioteca di scrittori. C’è un conflitto tra alcuni comfort e alcune altre comodità qui, e non si può avere la vostra comodità e mangiare troppo :'(
    • guardando avanti per la risposta ancora di più! BTW, Scott Meyers spiega il sempre-preferiscono-considerare-evitare-mai qualificazioni dei suoi Elementi qui. Così si potrebbe dire “evita” è appropriato o dovrebbe essere “considerare” o “attenzione” std::enable_if ?
    • +1. Btw c’è un interessante parlare di Scott Meyers perfetto inoltro dove ricopre il punto 2. Qui ci sono le diapositive dal talk, la sezione relativa a partire dalla diapositiva 8.
    • Grazie per la tua risposta, anche se non rispondere alla domanda circa l’utilizzo, in funzione di firme, ma criticare enable_if in generale.
    • Ci sarà una risposta separato da voi, dovrei aspettare?
    • Ho postato una risposta breve affrontare la tua domanda, e affrontare il problema di “SFINAE o non SFINAE” in un post sul blog invece (è un po ‘ off-topic su questa questione). Appena ho il tempo di finire, voglio dire.
    • SFINAE è “fragile”? Cosa????
    • relativo rispetto tag dispacciamento, chiamerei SFINAE “fragile”. Forse “sottile” è una parola migliore?
    • “le diverse condizioni di impiego più enable_if sovraccarichi non hanno alcuna sovrapposizione alla Definizione di regole (ODR)” Certo, sarebbe ambiguo al sovraccarico di risoluzione, ma che cosa gli ODR ha a che fare con questo?
    • grazie, aggiornato.
    • Molto buona risposta, grazie per la condivisione. Tutti e tre i punti citati sono spesso trascurati, specialmente il punto #1.

  3. 4

    Che la soluzione dovrebbe essere preferito e perché dovrei evitare altri?

    • Il parametro di modello

      • È utilizzabile in Costruttori.
      • È utilizzabile in definiti dall’utente operatore di conversione.
      • Si richiede C++11 o versione successiva.
      • È IMO, il più leggibile.
      • Potrebbe facilmente essere usato scorrettamente 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 correggere std::enable_if_t<cond, int>::type = 0

    • tipo di ritorno:

      • Non può essere utilizzato in un costruttore. (nessun tipo di ritorno)
      • Non può essere utilizzato in definiti dall’utente operatore di conversione. (non deducibile)
      • È possibile utilizzare pre-C++11.
      • Secondo più leggibile IMO.
    • Ultimo, in funzione del parametro:

      • È possibile utilizzare pre-C++11.
      • È utilizzabile in Costruttori.
      • Non può essere utilizzato in definiti dall’utente operatore di conversione. (senza parametri)
      • Non può essere utilizzato nei metodi con numero fisso di argomenti (unario/operatori binari +, -, *, …)
      • Può essere tranquillamente utilizzare in eredità (vedi sotto).
      • Cambiare la firma della funzione (fondamentalmente un extra come ultimo argomento void* = nullptr) (così puntatore a funzione sarebbe diverso, e così via)

    Ci sono differenze per i membri e non membri in funzione dei modelli?

    Ci sono sottili differenze con ereditarietà e using:

    Secondo il using-declarator (sottolineatura mia):

    spazio dei nomi.udecl

    L’insieme di dichiarazioni introdotto da usare-la dichiarazione è trovato eseguendo qualificato ricerca per nome ([base.di ricerca.qual], [classe.membro.ricerca]) per il nome in uso-dichiarazione, escluse le funzioni nascoste, come descritto di seguito.

    Quando, usando un dichiaratore di porta dichiarazioni da una classe base in una classe derivata, le funzioni membro e membro funzione di modelli in classe derivata sostituire e/o nascondere le funzioni membro e funzione membro modelli con lo stesso nome, il parametro del tipo di elenco, cv-qualifica, e ref-qualifier (se del caso) in una classe di base (e non conflittuali). Tali nascosti o sostituiti dichiarazioni sono esclusi dal set di dichiarazioni introdotto da usare-dichiarazione in tal senso.

    Per entrambi argomento di modello e tipo di ritorno, i metodi sono nascosti è il 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 erroneamente trova la funzione di base).

    Mentre con argomento simile scenario opere:

    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) {}
    };

    Demo

Lascia un commento