2014-10-01 10 views
15

Herb Sutter's Ritorno alle origini! La presentazione di Essentials of Modern C++ in CppCon ha discusso diverse opzioni per il passaggio dei parametri e ha confrontato le loro prestazioni rispetto alla facilità di scrittura/insegnamento. L'opzione 'avanzato' (che fornisce le migliori prestazioni in tutti i casi testati, ma troppo difficile per la maggior parte degli sviluppatori di scrivere) è stato l'inoltro perfetto, con l'esempio dato (PDF, pg. 28):Qual è il vincolo `enable_if` corretto su un inoltro perfetto?

class employee { 
    std::string name_; 

public: 
    template <class String, 
       class = std::enable_if_t<!std::is_same<std::decay_t<String>, 
                std::string>::value>> 
    void set_name(String &&name) noexcept(
     std::is_nothrow_assignable<std::string &, String>::value) { 
     name_ = std::forward<String>(name); 
    } 
}; 

L'esempio utilizza una funzione template con l'inoltro riferimento, con il parametro modello String vincolato utilizzando enable_if. Tuttavia, il vincolo sembra essere errato: sembra voler dire che questo metodo può essere utilizzato solo se il tipo String non è un std::string, il che non ha senso. Ciò significherebbe che questo membro std::string può essere impostato utilizzando qualsiasi valore tranne e un valore std::string.

using namespace std::string_literals; 

employee e; 
e.set_name("Bob"s); // error 

Una spiegazione ho ritenuto fosse che c'è un semplice errore di battitura e il vincolo era destinato ad essere std::is_same<std::decay_t<String>, std::string>::value invece di !std::is_same<std::decay_t<String>, std::string>::value. Tuttavia ciò implicherebbe che il setter non funzioni con, per esempio, const char * e ovviamente era destinato a funzionare con questo tipo dato che questo è uno dei casi testati nella presentazione.

Mi sembra che il vincolo corretta è più simile a:

consentendo tutto ciò che può essere assegnato al membro per essere utilizzato con il setter.

Ho il vincolo giusto? Ci sono altri miglioramenti che possono essere apportati? C'è qualche spiegazione per il vincolo originale, forse è stato preso fuori dal contesto?


Inoltre mi chiedo se le complicate parti, 'unteachable' di questa dichiarazione sono davvero così vantaggioso. Dal momento che non stiamo usando sovraccaricare possiamo semplicemente fare affidamento sulla normale modello di istanza:

template <class String> 
void set_name(String &&name) noexcept(
    std::is_nothrow_assignable<decltype((name_)), String>::value) { 
    name_ = std::forward<String>(name); 
} 

E naturalmente c'è un certo dibattito sul fatto se noexcept conta davvero, un po 'dicendo di non preoccuparsi troppo su di esso tranne che per spostare/primitive di swap :

template <class String> 
void set_name(String &&name) { 
    name_ = std::forward<String>(name); 
} 

Forse con i concetti non sarebbe irragionevolmente difficile vincolare il modello, solo per i messaggi di errore migliorati.

template <class String> 
    requires std::is_assignable<decltype((name_)), String>::value 
void set_name(String &&name) { 
    name_ = std::forward<String>(name); 
} 

Questo sarebbe ancora degli inconvenienti che non può essere virtuale e che deve essere in un'intestazione (anche se si spera moduli finiranno rendering che discutibile), ma questo sembra abbastanza insegnabile.

+0

Beh, qualcuno nella stanza ha detto che si tratta di un vincolo non corretto e non accetta stringhe letterali - forse possiamo cc @HowardHinnant? Non sembra nemmeno giusto per me. [Video del talk] (http://youtu.be/xnqTKD8uD64?t=1h15m21s) – dyp

+0

@PiotrS .: MSVC non ha alias, quindi ho dimenticato:/ –

+0

@MooingDuck Hey, VS2013 supporta gli alias e i loro strumenti stdlib gli alias di tipo_trait C++ 14. – bames53

risposta

3

Penso che probabilmente hai ragione, ma nell'interesse di non scrivere una "risposta" che è semplicemente "Sono d'accordo", lo proporrò invece che controllerà l'assegnazione in base ai tipi corretti - che si tratti di un lval, rval, const, qualunque sia:

template <class String> 
auto set_name(String&& name) 
-> decltype(name_ = std::forward<String>(name), void()) { 
    name_ = std::forward<String>(name); 
} 
+0

Ovviamente sarà un po 'più brutto quando si aggiunge la clausola 'noexcept'. –

4

una spiegazione ho ritenuto fosse che c'è un semplice errore di battitura e il vincolo era destinato ad essere std::is_same<std::decay_t<String>, std::string>::value invece di !std::is_same<std::decay_t<String>, std::string>::value.

Sì, il vincolo di destra che è stato presentato sullo schermo era is_same, non !is_same. Sembra che ci sia un errore di battitura nella tua diapositiva.


Tuttavia, che implicherebbe che il setter non funziona con, per esempio, const char *

Sì, e credo che questo è stato fatto di proposito. Quando una stringa letterale come "foo" viene passato a una funzione che accetta un riferimento universale, quindi il tipo dedotta non un puntatore (dato array decadimento ai puntatori solo quando colto nel parametro template dal valore), invece, è a const char(&)[N]. Detto questo, ogni chiamata a set_name con la stringa letterale di lunghezza diversa sarebbe un'istanza di una nuova set_name di specializzazione, come:

void set_name(const char (&name)[4]); // set_name("foo"); 
void set_name(const char (&name)[5]); // set_name("foof"); 
void set_name(const char (&name)[7]); // set_name("foofoo"); 

Il vincolo si suppone per definire il riferimento universale in modo che accetti e ne deduce solo sia std::string tipi per argomenti rvalue, oppure cv-std::string& per argomenti lvalue (è per questo che è std::decay edita prima di confrontare a std::string in quella condizione std::is_same).


ovviamente era destinato a lavorare con questo tipo dato che questo è uno dei casi esaminati nella presentazione.

credo che la versione testata (4) non è stato costretto (si noti che è stato chiamato String&& + inoltro perfetto), quindi potrebbe essere semplice come:

template <typename String> 
void set_name(String&& name) 
{ 
    _name = std::forward<String>(name); 
} 

in modo che quando una stringa letterale viene passato, non costruisce l'istanza std::string prima della chiamata di funzione come farebbe la versione non basata su modelli (allocando inutilmente memoria nel codice di callee solo per creare un valore temporaneo std::string che verrebbe eventualmente spostato nella destinazione eventualmente preallocata come _name):

void set_name(const std::string& name); 
void set_name(std::string&& name); 

Mi sembra che il vincolo corretta è più simile a: std::enable_if_t<std::is_assignable<decltype((name_)), String>::value>>

No, come ho scritto, non credo che l'intento era quello di vincolare il set_name per accettare i tipi assegnabili a std::string. Ancora una volta, quell'originale std::enable_if è lì per avere una singola implementazione della funzione set_name che accetta un riferimento universale accettando solo i valori e i valori di std::string (e niente di più sebbene questo sia un modello). Nella tua versione di std::enable_if il passaggio di qualcosa che non è assegnabile a std::string produrrebbe un errore, indipendentemente dal fatto che la difesa sia o meno provata a farlo. Si noti che alla fine potremmo semplicemente spostare l'argomento a se è valore di riferimento non costante, quindi controllare l'assegnabilità è inutile tranne che nelle situazioni in cui non si utilizza un SFINAE per escludere tale funzione dalla risoluzione di sovraccarico a favore di altro sovraccarico .


Qual è la corretta enable_if vincolo perfetto setter inoltro?

template <class String, 
      class = std::enable_if_t<std::is_same<std::decay_t<String>, 
               std::string>::value>> 

o nessun vincolo a tutti se non causerà alcun calo di prestazioni (proprio come passare stringhe letterali).

+0

* "Non penso che l'intento fosse quello di limitare il set_name ad accettare i tipi assegnabili a' std :: string' "* Se lo confrontiamo con le altre soluzioni, dovrebbe almeno essere implicitamente convertibile in' std :: string 'per consentire le stesse conversioni delle altre soluzioni. Ma ciò sembra inappropriato, dal momento che * non * vogliamo eseguire quella conversione implicita prima dell'assegnazione. – dyp

+0

@dyp: questa è una buona ragione per cui il tipo è vincolato solo a 'std :: string's, non a tipi * assegnabili * a stringa, è questo che intendi? –

+0

Beh, intendo le altre soluzioni permettono 'x.set_name (" ciao ");', mentre questo no - '" ciao "' non è un 'std :: string', ma è implicitamente convertibile in' std :: string && 'e' std :: string const & 'e' std :: string'. – dyp

1

Ho cercato di inserire questi pensieri in un commento, ma non si adatterebbero. Presumo di scrivere questo come sono menzionato sia nei commenti sopra, sia nel bel discorso di Herb.

Mi dispiace di essere in ritardo per la festa. Ho rivisto i miei appunti e in effetti sono colpevole di raccomandare a Herb il vincolo originale per l'opzione 4 meno il errante !. Nessuno è perfetto, come confermerà sicuramente mia moglie, meno di tutti me. :-)

Promemoria: il punto di Herb (che sono d'accordo con) è iniziare con il semplice C++ 98/03 consigli

set_name(const string& name); 

e passare da lì solo se necessario. L'opzione # 4 è un bel po 'di movimento. Se consideriamo l'opzione n. 4, siamo al conteggio di carichi, negozi e allocazioni in una parte critica dell'applicazione. Dobbiamo assegnare name a name_ il più velocemente possibile.Se siamo qui, la leggibilità del codice è molto meno importante delle prestazioni, anche se la correttezza è ancora valida.

Fornire alcun vincolo (per l'opzione n. 4) ritengo che sia leggermente errato. Se qualche altro codice ha tentato di limitare se stesso con se o non poteva chiamare employee::set_name, si potrebbe ottenere la risposta sbagliata se set_name non è vincolato a tutti:

template <class String> 
auto foo(employee& e, String&& name) 
-> decltype(e.set_name(std::forward<String>(name)), void()) { 
    e.set_name(std::forward<String>(name)); 
    // ... 

Se set_name è vincolato e String deduce a qualche tipo completamente indipendenti X, il vincolo precedente su foo include erroneamente questa istanziazione di foo nel set di sovraccarico. E la correttezza è ancora re ...

E se volessimo assegnare un singolo carattere a name_? Dì A. Dovrebbe essere permesso? Dovrebbe essere veloce malvagia?

e.set_name('A'); 

Bene, perché no ?! std::string deve solo ad un operatore di assegnazione:

basic_string& operator=(value_type c); 

ma nota che non v'è alcun costruttore corrispondente:

basic_string(value_type c); // This does not exist 

Pertanto is_convertible<char, string>{} è false, ma is_assignable<string, char>{} è true.

E 'Non un errore logico per cercare di impostare il nome di un string con un char (a meno che non si desidera aggiungere la documentazione per employee che dice così). Così, anche se il C++ 98/03 implementazione originale non ha permesso la sintassi:

e.set_name('A'); 

Ha fatto consentire la stessa operazione logica in maniera meno efficiente:

e.set_name(std::string(1, 'A')); 

E si tratta di opzione n. 4 perché desideriamo disperatamente ottimizzare questa cosa nella massima misura possibile.

Per queste ragioni, penso che is_assignable sia il tratto migliore per vincolare questa funzione. E stilisticamente trovo lo Barry's technique for spelling this constraint quite acceptable. Quindi è qui che va il mio voto.

Si noti inoltre che employee e std::string qui sono solo esempi nel discorso di Herb. Sono stand-in per i tipi con codice. Questo consiglio è destinato a generalizzare il codice che devi affrontare.

+0

Questo è un buon punto sul fatto di aver bisogno di un vincolo per evitare di assegnare un int a 'name_', tuttavia mi sembra che sia un caso particolare a causa del fatto che viene applicata una conversione implicita da' int' a 'char'. Idealmente la classe stringa sarebbe in grado di disabilitare le conversioni implicite in questo caso, ma penso che almeno possiamo fare qualcosa in 'employee :: set_name'. Che dire di [questo] (http://coliru.stacked-crooked.com/a/b25e0861fbeb7c00)? O forse usare le parentesi graffe per questo è troppo sottile ed è quindi altrettanto intrattabile dei vincoli di SFINAE? – bames53

+0

@ bames53: Il mio errore nell'uso di 'int' come esempio di" altro tipo "nel mio esempio. Avrei dovuto usare 'class rutabaga' e ho dimostrato che' rutabaga' non ha conversioni su 'std :: string'.Tuttavia a volte 'dipendente' mangia rutabaga's, e quindi è concepibile che' pippo' venga sovraccaricato su 'dipendente' e' rutabaga', o qualcosa che 'rutabaga' converta implicitamente in un tipo di' verdura', o un modello parametro che rappresenta un 'vegetale'. Cioè con SFINAE estesa, * qualsiasi * espressione può essere usata come un vincolo, anche 'e.set_name'. –

+0

Ma se vincolate con 'is_assignable', il vostro' foo' con 'String = int' sarà ancora nel set di sovraccarico, perché' int' (e 'double', ecc.) È convertibile in' char'. –

Problemi correlati