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


70

C ++ 17 introduce l' [[nodiscard]]attributo, che consente ai programmatori di contrassegnare le funzioni in modo tale che il compilatore produca un avviso 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 funzionalità nella proposta originale e so che C ++ 20 aggiungerà l'attributo a funzioni standard come std::vector::empty, i cui nomi non trasmettono un significato inequivocabile per quanto riguarda il valore restituito.

È una funzionalità interessante e utile. In effetti, sembra quasi troppo utile. Ovunque io legga [[nodiscard]], le persone ne discutono come se lo aggiungessi a poche funzioni o tipi selezionati e dimentichino il resto. Ma perché un valore non scartabile dovrebbe essere un caso speciale, specialmente quando si scrive un nuovo codice? Un valore di ritorno scartato non è in genere un bug o almeno uno spreco di risorse?

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

Se è così, allora perché non aggiungere [[nodiscard]]il proprio codice non legacy a quasi ogni singola non voidfunzione e quasi ogni singolo tipo di classe?

Ho provato a farlo nel mio codice, e funziona benissimo, tranne che è così terribilmente dettagliato che inizia a sembrare Java. Sembrerebbe molto più naturale fare in modo che i compilatori avvertano dei valori di ritorno scartati per impostazione predefinita, tranne per i pochi altri casi in cui si contrassegna la propria intenzione [*] .

Dato che ho visto zero discussioni su questa possibilità in proposte standard, post di blog, domande Stack Overflow o in qualsiasi altra parte di Internet, mi devo perdere qualcosa.

Perché tali meccanismi non avrebbero senso nel nuovo codice C ++? La verbosità è l'unica ragione per non usarla [[nodiscard]]quasi ovunque?


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


22
Bene, ci sono molte funzioni in cui il valore di ritorno può essere scartato sensibilmente. operator =per esempio. E std::map::insert.
user253751

2
@immibis: True. La libreria standard è piena di tali funzioni. Ma nel mio codice, tendono ad essere estremamente rari; pensi che sia una questione di codice della biblioteca rispetto al codice dell'applicazione?
Christian Hackl,

9
"... terribilmente prolisso che inizia a sembrare Java ..." - leggere questo in una domanda sul C ++ mi ha fatto contrarre un po ': - /
Marco13

3
@ChristianHackl I commenti probabilmente non sono il posto giusto per "bashing linguistico" e, naturalmente, anche Java è dettagliato rispetto ad altre lingue. Ma i file di intestazione, gli operatori di assegnazione, i costruttori di copie e il corretto utilizzo constpossono gonfiare una classe altrimenti "semplice" (o piuttosto "semplice oggetto di dati vecchi") considerevolmente in C ++.
Marco13

2
@ Marco13: non posso affrontare così tanti punti in un commento. In breve: Java non ha file di intestazione, il che riduce il conteggio dei file ma aumenta l'accoppiamento; OTOH ti costringe a mettere ogni classe di primo livello nel suo file e ogni funzione in una classe. In C ++, quasi mai implementate operatori di assegnazione e copiate voi stessi i costruttori; quelle funzioni sono più rilevanti per le classi di librerie standard come std::vectoro std::unique_ptr, di cui hai solo bisogno di membri di dati nella definizione della classe. Ho lavorato con entrambe le lingue; Java è un linguaggio okay, ma è più dettagliato.
Christian Hackl,

Risposte:


73

Nel nuovo codice che non deve essere compatibile con gli standard più vecchi, usa quell'attributo ovunque sia ragionevole. Ma per C ++, [[nodiscard]]rende un default non valido. Tu suggerisci:

Sembrerebbe molto più naturale fare in modo che i compilatori avvertano dei valori di ritorno scartati per impostazione predefinita, tranne per i pochi altri casi in cui si contrassegna la propria intenzione.

Ciò causerebbe improvvisamente l'emissione di un sacco di avvertenze da parte del codice esistente esistente. Mentre un tale cambiamento potrebbe tecnicamente essere considerato retrocompatibile poiché qualsiasi codice esistente si compila ancora con successo, nella pratica si tratterebbe di un enorme cambiamento di semantica.

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, avvertire per impostazione predefinita sarebbe sensato. Ad esempio, il linguaggio Nim richiede che i valori non necessari vengano scartati in modo esplicito - questo sarebbe simile a racchiudere ogni istruzione di espressione in C ++ con un cast (void)(...).

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

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

  • se il valore di ritorno deve essere verificato, ad es. per un'interfaccia di tipo C che restituisce codici di errore invece di lanciare. 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 un simile avvertimento sarebbe fuorviante. Per esempio:

  • Considerare un tipo di dati di coda con un .pop()metodo che rimuove un elemento e restituisce una copia dell'elemento rimosso. 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.

    Si noti che in alcuni casi, una copia deve essere fatta comunque, quindi grazie a RVO la restituzione della copia sarebbe gratuita.

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

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

Nota che il tuo nuovo brillante codice C ++ 17 (dove puoi usare questi attributi) può ancora essere compilato insieme a librerie che hanno come target gli standard C ++ precedenti. Questa compatibilità con le versioni precedenti è cruciale per l'ecosistema C / C ++. Pertanto, l'impostazione predefinita di nodiscard comporterebbe molti avvisi per casi d'uso tipici e idiomatici, avvisi che non è possibile correggere senza modifiche di vasta portata al codice sorgente della libreria.

Probabilmente, il problema qui non è il cambio di semantica ma che le funzionalità 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 di intestazione, tale modifica sarebbe più realistica.


6
Ho votato questo perché contiene utili approfondimenti. Tuttavia, non sono ancora sicuro che risponda davvero alla domanda. Ri sono funzioni impure come popo operator<<, quelle sarebbero esplicitamente contrassegnate dal mio [[maydiscard]]attributo immaginario , quindi non verrebbero generati avvisi. Stai dicendo che tali funzioni sono generalmente molto più comuni di quanto mi piacerebbe credere, o molto più comuni in generale che nei miei codebase? Il tuo primo paragrafo sul codice esistente è certamente vero, ma mi chiedo ancora se qualcun altro stia attualmente sborsando tutto il suo nuovo codice con [[nodiscard]], e se no, perché no.
Christian Hackl,

23
@ChristianHackl: C'è stato un tempo nella storia di C ++ in cui era considerato una buona pratica restituire valori come const. Non puoi dire returns_an_int() = 0, quindi perché dovrebbe returns_a_str() = ""funzionare? C ++ 11 ha mostrato che questa era in realtà un'idea terribile, perché ora tutto questo codice proibiva le mosse perché i valori erano const. Penso che il corretto take-away di quella lezione sia "non dipende da te cosa fanno i tuoi chiamanti con il tuo risultato". Ignorare il risultato è una cosa perfettamente valida che i chiamanti potrebbero voler fare e, a meno che tu non sappia che è sbagliato (una barra molto alta), non dovresti bloccarlo.
GManNickG,

13
Un nodiscard empty()è anche sensato perché il nome è abbastanza ambiguo: è un predicato is_empty()o è un mutatore clear()? In genere non ti aspetteresti che un simile mutatore restituisca qualcosa, quindi un avviso su un valore di ritorno può avvisare il programmatore di un problema. Con un nome più chiaro, questo attributo non sarebbe necessario.
amon,

13
@ChristianHackl: Come altri hanno già detto, penso che le funzioni pure siano un buon esempio di quando questo dovrebbe essere usato. Farei un ulteriore passo avanti e direi che dovrebbe esserci un [[pure]]attributo, e ciò dovrebbe implicare [[nodiscard]]. In questo modo è chiaro perché questo risultato non debba essere scartato. Ma oltre alle pure funzioni, non riesco a pensare a un'altra classe di funzioni che dovrebbe avere questo comportamento. E la maggior parte delle funzioni non sono pure, quindi non credo che nodiscard come predefinito sia la scelta giusta.
GManNickG

5
@GManNickG: dopo aver tentato e fallito il debug del codice che ha ignorato i codici di errore, credo fermamente che la maggior parte dei codici di errore debba essere controllata per impostazione predefinita.
Mooing Duck il

9

Ragioni che non avrei [[nodiscard]]quasi ovunque:

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

Ora, se lo rendessi predefinito, forzerai tutti gli sviluppatori di librerie a forzare tutti i loro utenti a non scartare i valori di ritorno. Sarebbe orribile. O li costringi ad aggiungere [[maydiscard]]innumerevoli funzioni, che è anche un po 'orribile.


0

Ad esempio: l'operatore << ha un valore di ritorno che dipende dalla chiamata o assolutamente necessaria o assolutamente inutile. (std :: cout << x << y, il primo è necessario perché restituisce il flusso, il secondo non serve affatto). Ora confronta con printf dove tutti scartano il valore restituito che è lì per il controllo degli errori, ma poi l'operatore << non ha alcun controllo degli errori, quindi non può essere stato così utile in primo luogo. Quindi in entrambi i casi nodiscard sarebbe solo dannoso.


Questo risponde a una domanda che non è stata posta, vale a dire "Qual è la ragione per non usare [[nodiscard]]ovunque?".
Christian Hackl,
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.