Qual è la ragione per non usare C ++ 17 [[nodiscard]] quasi ovunque nel nuovo codice?

57

C ++ 17 introduce l'attributo [[nodiscard]] , che consente ai programmatori di contrassegnare le funzioni in modo che il compilatore produca un avvertimento se l'oggetto restituito viene scartato da un chiamante; lo stesso attributo può essere aggiunto a un intero tipo di classe.

Ho letto la motivazione di questa funzione nell'originale proposta , e so che C ++ 20 aggiungerà l'attributo a funzioni standard come std::vector::empty , i cui nomi non trasmettono un significato non ambiguo riguardo al valore restituito.

È una funzione interessante e interessante. In effetti, sembra quasi too utile. Ovunque legga [[nodiscard]] , le persone lo discutono come se lo si aggiungesse a poche funzioni o tipi selezionati e si dimenticasse del resto. Ma perché un valore non trascurabile dovrebbe essere un caso speciale, specialmente quando si scrive un nuovo codice? Non è un valore di ritorno scartato tipicamente un bug o almeno uno spreco di risorse?

E non è uno dei principi di progettazione di C ++ stesso che il compilatore dovrebbe intercettare quanti più errori possibile?

In tal caso, allora perché non aggiungere [[nodiscard]] nel tuo codice non legacy a quasi tutte le singole funzioni non- void e quasi ogni singolo tipo di classe?

Ho provato a farlo nel mio codice, e funziona bene, tranne che è così terribilmente prolisso che inizia a sentirsi come Java. Sembrerebbe molto più naturale rendere i compilatori avvisare sui valori di ritorno scartati di default tranne per gli altri pochi casi in cui contrassegni la tua intenzione [*] .

Come ho visto zero discussioni su questa possibilità in proposte standard, post di blog, domande Stack Overflow o altrove su Internet, mi manca qualcosa.

Perché una tale meccanica non avrebbe senso nel nuovo codice C ++? La verbosità è l'unica ragione per non utilizzare [[nodiscard]] quasi ovunque?

[*] In teoria, potresti avere qualcosa come un attributo [[maydiscard]] , che potrebbe anche essere aggiunto retroattivamente a funzioni come printf nelle implementazioni di librerie standard.

    
posta Christian Hackl 30.12.2017 - 18:08
fonte

3 risposte

60

In un nuovo codice che non deve essere compatibile con gli standard precedenti, usa quell'attributo ovunque sia ragionevole. Ma per C ++, [[nodiscard]] rende un cattivo valore predefinito. Suggerisci:

It would seem much more natural to make compilers warn about discarded return values by default except for the few other cases where you mark your intention.

Ciò causerebbe improvvisamente la presenza di codice esistente e corretto per emettere molti avvertimenti. Sebbene un tale cambiamento possa tecnicamente essere considerato retrocompatibile dal momento che qualsiasi codice esistente viene ancora compilato con successo, sarebbe un enorme cambiamento di semantica nella pratica.

Le decisioni di progettazione per un linguaggio esistente e maturo con una base di codice molto ampia sono necessariamente diverse dalle decisioni di progettazione per un linguaggio completamente nuovo. Se questa fosse una nuova lingua, l'avviso di default sarebbe ragionevole. Ad esempio, la lingua Nim richiede che i valori non necessari vengano scartati esplicitamente - questo sarebbe simile al wrapping di ogni espressione di espressione in C ++ con un cast (void)(...) .

Un attributo [[nodiscard]] è più utile in due casi:

  • se una funzione non ha effetti oltre a restituire un determinato risultato, cioè è pura. Se il risultato non viene utilizzato, la chiamata è certamente inutile. D'altra parte, scartare il risultato non sarebbe errato.

  • se il valore restituito deve essere verificato, ad es. per un'interfaccia simile a C che restituisce i codici di errore anziché il lancio. Questo è il caso d'uso principale. Per il C ++ idiomatico, sarà abbastanza raro.

Questi due casi lasciano un enorme spazio di funzioni impure che restituiscono un valore, in cui tale avviso sarebbe fuorviante. Ad esempio:

  • Considera un tipo di dati di coda con un metodo .pop() che rimuove un elemento e restituisce una copia dell'elemento rimosso. Un tale metodo è spesso conveniente. Tuttavia, ci sono alcuni casi in cui vogliamo solo rimuovere l'elemento, senza ottenere una copia. Questo è perfettamente legittimo e un avvertimento non sarebbe utile. Un design diverso (come std::vector ) divide queste responsabilità, ma ha altri compromessi.

    Nota che in alcuni casi, una copia deve essere fatta comunque così, grazie a RVO che restituisce la copia sarebbe gratuita.

  • Considera interfacce fluenti, in cui ogni operazione restituisce l'oggetto in modo da poter eseguire ulteriori operazioni. In C ++, l'esempio più comune è l'operatore di inserimento del flusso << . Sarebbe estremamente ingombrante aggiungere un attributo [[nodiscard]] a ogni% sovraccarico di<<.

Questi esempi dimostrano che la compilazione di codice C ++ idiomatico senza avvisi in un linguaggio "C ++ 17 con nodiscard di default" sarebbe alquanto noiosa.

Nota che il tuo nuovo codice C ++ 17 (dove puoi usare questi attributi) può ancora essere compilato insieme a librerie che mirano agli standard C ++ più vecchi. Questa retrocompatibilità è fondamentale per l'ecosistema C / C ++. Quindi, facendo il nodisco, l'impostazione predefinita risulterebbe in molti avvisi per i tipici casi d'uso idiomatici: avvertimenti che non è possibile correggere senza modifiche di vasta portata al codice sorgente della biblioteca.

Probabilmente, il problema qui non è il cambiamento della semantica ma che le caratteristiche di ogni standard C ++ si applicano su un ambito per unità di compilazione e non su un ambito per file. Se / quando qualche futuro standard C ++ si allontana dai file header, tale cambiamento sarebbe più realistico.

    
risposta data 30.12.2017 - 18:36
fonte
10

[[nodiscard]] è solo un suggerimento

Come ha spiegato Andrew Tomazos in P0189R0 , [[nodiscard]] è piuttosto un suggerimento del designer, non il requisito. Un utente potrebbe voler eliminare intenzionalmente i risultati della funzione e il designer non dovrebbe negare le sue invenzioni.

The existing practice demonstrates there are cases when the programmer intentionally wants to discard the result of a nodiscard function, even though in most cases they do not. The existing nodiscard is a hint from the function designer to the function user, that immediately destroying the result is most likely not what you want, but it isn’t a straight­jacket and isn’t used as such.

In seguito a questa dichiarazione, entrambi i casi in cui l'attributo [[nodiscard]] potrebbe causare un errore o la parola chiave nodiscard potrebbe causare un comportamento indefinito, sono stati attivati a favore dell'attributo [[nodiscard]] causando solo un avviso.

Ulteriori [[nodiscard]] usi

Oltre alla risposta di @ amon, come ha sottolineato Herb Sutter in Rapporto di viaggio: la riunione sugli standard ISO C ++ invernali , l'attributo [[nodiscard]] ha due ulteriori utilizzi:

  • Annotare un tipo per applicare a tutti gli usi di quel tipo come valore restituito, come nel caso di std::async , dove ignorare il valore restituito significherebbe non essere affatto asincroni.
  • Indicazione di una differenza per prevenire l'errore , come nel caso in cui usi empty() e clear() , quindi per esempio l'utente saprà che chiamare empty() non ha effetti collaterali se chiamato per errore.

Canonical examples include the return value of std::async where ignoring the return value would mean being not async at all, and the return value of a container’s empty() function where people sometimes confuse empty() and clear() and we don’t want them to think calling empty() has side effects if they call it by mistake.

[[nodiscard]] significato implicito per risorse che richiedono funzioni

  • Si noti che la seguente sezione è correlata solo al compilatore di Visual Studio con /analyze o altrimenti abilitato C6031 warning.

La documentazione di Visual Studio su C6031 afferma che il significato di [[nodiscard]] è implicito per risorse che richiedono funzioni, come disco, rete o memoria.

In general, it is not safe to assume that a call to function requiring disk, network, memory, or other resources will always succeed.

Risulta che il significato di [[nodiscard]] è implicito per tali funzioni, e il chiamante dovrebbe sempre prestare attenzione al valore del risultato. Scartando il valore di tale funzione viene emesso anche un avviso C6031 se abilitato ( ie compilato con /analyze ).

Esempio

Nell'esempio seguente, l'eliminazione del std::fopen risultato causerà anche un avviso di C6031 anche se std::fopen non ha un attributo [[nodiscard]] esplicito.

           fopen("/._1", "r"); // C6031
FILE* Fp = fopen("/._2", "r"); // OK

The caller should always check the return value and handle error cases appropriately.

esteso [[nodiscard]] : deve ispezionare il risultato

Visual Studio va ancora oltre, consentendo la funzione di marcatura con _Must_inspect_result_ che non solo disabilita il valore di scarto, ma costringe l'utente a controllarne il valore piuttosto che a farlo con void .

Also consider using the _Must_inspect_result_ annotation, which checks that the value is examined in a useful way.

_Must_inspect_result_ valore di ritorno annotato deve essere più inspected o verrà generato un avviso.

"Inspection" includes whether it is used in a conditional expression, is assigned to an output parameter or global, or is passed as a parameter.

Esempio

Nell'esempio seguente, il risultato dell'eliminazione causerà l'avvertimento C6031 , ma l'assegnazione senza ispezione determinerà un avviso C28193 . Solo quando esamineremo definitivamente i risultati, non verrà emesso alcun avviso.

_Must_inspect_result_ bool f__Must_inspect_result_(){ return true; };

           f__Must_inspect_result_();         // C6031
bool  b2 = f__Must_inspect_result_();         // C28193
bool  b3 = f__Must_inspect_result_(); if(b3); // OK

In some rare instances, the return value is intentionally not used. The most common of these instances is where a string length is returned but not actually used before some other test is made.

I miei pensieri: [[maybe_noinspect]]

Poiché è possibile differenziare due tipi di [[nodiscard]] :

  • standard significato: avviso di rilascio su scartato risultato
  • esteso significato: avviso di rilascio su non ispezionato risultato

Chiederò una domanda retorica: perché non utilizzare entrambi gli attributi [[nodiscard]] e [[maybe_noinspect]] ?

  • [[nodiscard]] attributo potrebbe rimanere invariato, il che significa che il risultato non deve essere scartato, ma genera un avviso se il valore restituito non è rispettato, finché è presente l'attributo [[maybe_noinspect]] .
  • [[maybe_noinspect]] attributo potrebbe significare che il valore restituito potrebbe non essere rispettato, ignorando l'avviso causato dal valore non osservato, ma il valore non rispettato restituito con l'attributo [[nodiscard]] .

[[maybe_noinspect]] può essere utilizzato sia insieme a [[nodiscard]] , sia da solo, possibilmente implicando l'attributo [[nodiscard]] nel secondo caso.

Esempio

Se avessimo [[maybe_noinspect]] e implicherebbe [[nodiscard]] , potremmo estendere il significato di [[nodiscard]] , con la possibilità di trasformarlo in standard significando indietro di [[maybe_noinspect]] .

[[nodiscard]]       bool f_nodiscard()      { return true; };
[[maybe_noinspect]] bool f_maybe_noinspect(){ return true; };

          f_nodiscard();               // Warning, value is discarded
bool b1 = f_nodiscard();               // Warning, value is uninspected
bool b2 = f_nodiscard(); if(b2);       // OK,      value is inspected

          f_maybe_noinspect();         // Warning, value is discarded
bool b3 = f_maybe_noinspect();         // OK,      value may be not inspected
bool b4 = f_maybe_noinspect(); if(b4); // OK,      value is inspected
    
risposta data 31.12.2017 - 12:41
fonte
6

Motivi per cui non vorrei [[nodiscard]] quasi ovunque:

  1. (maggiore :) Ciò introdurrebbe modo troppo rumore nelle intestazioni.
  2. (maggiore :) Non mi sento di dover formulare ipotesi strongmente speculative sul codice di altre persone. Vuoi scartare il valore di ritorno che ti do? Bene, buttati fuori.
  3. (minore :) Garantire l'incompatibilità con C ++ 14

Ora, se lo hai reso predefinito, dovresti costringere tutti gli sviluppatori di librerie a forzare tutti i loro utenti a non-scartare i valori di ritorno. Sarebbe orribile. Oppure imponi loro di aggiungere [[maydiscard]] in innumerevoli funzioni, il che è anche un po 'orribile.

    
risposta data 31.12.2017 - 02:39
fonte

Leggi altre domande sui tag