Come si dovrebbe usare std :: opzionale?


134

Sto leggendo la documentazione di std::experimental::optionale ho una buona idea di ciò che fa, ma non capisco quando dovrei usarlo o come dovrei usarlo. Il sito non contiene ancora alcun esempio che mi rende più difficile comprendere il vero concetto di questo oggetto. Quando è std::optionaluna buona scelta da usare e come compensa ciò che non è stato trovato nello standard precedente (C ++ 11).


19
I documenti boost.optional potrebbero far luce.
juanchopanza,

Sembra che std :: unique_ptr possa generalmente servire gli stessi casi d'uso. Immagino che se hai qualcosa contro il nuovo, allora facoltativo potrebbe essere preferibile, ma mi sembra che (sviluppatori | app) con quel tipo di vista sul nuovo siano in una piccola minoranza ... AFAICT, opzionale NON è una grande idea. Per lo meno, la maggior parte di noi potrebbe vivere comodamente senza di essa. Personalmente, mi sentirei più a mio agio in un mondo in cui non devo scegliere tra unique_ptr e facoltativo. Chiamami pazzo, ma Zen of Python ha ragione: lascia che ci sia un modo giusto per fare qualcosa!
codice alleato

19
Non sempre vogliamo allocare qualcosa sull'heap che deve essere eliminato, quindi no unique_ptr non è un sostituto di facoltativo.
Krum,

5
@allyourcode Nessun puntatore è un sostituto per optional. Immagina di volerne uno optional<int>o addirittura <char>. Pensi davvero che sia necessario "Zen" per allocare, dereferenziare e quindi eliminare in modo dinamico - qualcosa che altrimenti non potrebbe richiedere alcuna allocazione e adattarsi in modo compatto nello stack, e forse anche in un registro?
underscore_d

1
Un'altra differenza, risultante senza dubbio dall'allocazione stack vs heap del valore contenuto, è che l'argomento template non può essere un tipo incompleto nel caso di std::optional(mentre può essere per std::unique_ptr). Più precisamente, la norma richiede che T [...] soddisfi i requisiti di Destructible .
dan_din_pantelimon

Risposte:


173

L'esempio più semplice che mi viene in mente:

std::optional<int> try_parse_int(std::string s)
{
    //try to parse an int from the given string,
    //and return "nothing" if you fail
}

La stessa cosa potrebbe essere realizzata con un argomento di riferimento (come nella firma seguente), ma l'utilizzo std::optionalrende la firma e l'utilizzo più piacevoli.

bool try_parse_int(std::string s, int& i);

Un altro modo in cui ciò potrebbe essere fatto è particolarmente negativo :

int* try_parse_int(std::string s); //return nullptr if fail

Ciò richiede l'allocazione dinamica della memoria, la preoccupazione per la proprietà, ecc. - Preferisci sempre una delle altre due firme sopra.


Un altro esempio:

class Contact
{
    std::optional<std::string> home_phone;
    std::optional<std::string> work_phone;
    std::optional<std::string> mobile_phone;
};

Questo è estremamente preferibile invece di avere qualcosa di simile a un std::unique_ptr<std::string>per ogni numero di telefono! std::optionalti dà la localizzazione dei dati, il che è ottimo per le prestazioni.


Un altro esempio:

template<typename Key, typename Value>
class Lookup
{
    std::optional<Value> get(Key key);
};

Se la ricerca non contiene una determinata chiave, possiamo semplicemente restituire "nessun valore".

Posso usarlo in questo modo:

Lookup<std::string, std::string> location_lookup;
std::string location = location_lookup.get("waldo").value_or("unknown");

Un altro esempio:

std::vector<std::pair<std::string, double>> search(
    std::string query,
    std::optional<int> max_count,
    std::optional<double> min_match_score);

Questo ha molto più senso di, diciamo, avere quattro sovraccarichi di funzioni che prendono ogni possibile combinazione di max_count(o no) e min_match_score(o no)!

Ha inoltre elimina la maledetta "Passo -1per max_countse non si desidera un limite" o "Passo std::numeric_limits<double>::min()per min_match_scorese non si desidera un punteggio minimo"!


Un altro esempio:

std::optional<int> find_in_string(std::string s, std::string query);

Se la stringa di query non è presente s, desidero "no int", non quale valore speciale qualcuno abbia deciso di utilizzare per questo scopo (-1?).


Per ulteriori esempi, è possibile consultare la boost::optional documentazione . boost::optionale std::optionalsarà sostanzialmente identico in termini di comportamento e utilizzo.


13
@gnzlbg std::optional<T>è solo un Te un bool. Le implementazioni delle funzioni membro sono estremamente semplici. Le prestazioni non dovrebbero davvero essere una preoccupazione quando la si utilizza: ci sono momenti in cui qualcosa è facoltativo, nel qual caso questo è spesso lo strumento corretto per il lavoro.
Timothy Shields,

8
@TimothyShields std::optional<T>è molto più complesso di così. Usa il posizionamento newe molte altre cose con allineamento e dimensioni adeguate per renderlo un tipo letterale (cioè usare con constexpr) tra le altre cose. L'ingenuità Te l' boolapproccio fallirebbero abbastanza rapidamente.
Rapptz,

15
@Rapptz Riga 256: union storage_t { unsigned char dummy_; T value_; ... }Riga 289: struct optional_base { bool init_; storage_t<T> storage_; ... }come mai non è "a Te a bool"? Concordo pienamente che l'implementazione sia molto delicata e non banale, ma concettualmente e concretamente il tipo è a Te a bool. "L'ingenuità Te l' boolapproccio fallirebbero abbastanza rapidamente." Come puoi fare questa affermazione guardando il codice?
Timothy Shields,

12
@Rapptz sta ancora conservando un bool e lo spazio per un int. L'unione è lì solo per fare in modo che l'opzione non costruisca una T se in realtà non è desiderata. È ancora struct{bool,maybe_t<T>}l'unione che è lì per non fare, il struct{bool,T}che costruirà una T in tutti i casi.
PeterT

12
@allyourcode Ottima domanda. Entrambi std::unique_ptr<T>e std::optional<T>in un certo senso svolgono il ruolo di "facoltativo T". Descriverei la differenza tra loro come "dettagli di implementazione": allocazioni extra, gestione della memoria, localizzazione dei dati, costo di spostamento, ecc. Non avrei mai avuto std::unique_ptr<int> try_parse_int(std::string s);ad esempio, perché causerebbe un'allocazione per ogni chiamata anche se non c'è motivo di . Non avrei mai una classe con un std::unique_ptr<double> limit;- perché fare un'allocazione e perdere la localizzazione dei dati?
Timothy Shields,

35

Un esempio è citato dal nuovo documento adottato: N3672, std :: opzionale :

 optional<int> str2int(string);    // converts int to string if possible

int get_int_from_user()
{
     string s;

     for (;;) {
         cin >> s;
         optional<int> o = str2int(s); // 'o' may or may not contain an int
         if (o) {                      // does optional contain a value?
            return *o;                  // use the value
         }
     }
}

13
Perché puoi passare le informazioni sul fatto che tu abbia una intgerarchia di chiamate ascendente o discendente, invece di passare un valore "fantasma" "assunto" per avere un significato di "errore".
Luis Machuca,

1
@Wiz Questo è in realtà un ottimo esempio. (A) consente di str2int()implementare la conversione come desidera, (B) indipendentemente dal metodo per ottenere il string s, e (C) trasmette il significato completo tramite lo optional<int>stupido numero magico, bool/ riferimento o un modo basato su allocazione dinamica facendolo.
underscore_d,

10

ma non capisco quando dovrei usarlo o come dovrei usarlo.

Considerare quando si scrive un'API e si desidera esprimere che il valore "non avere un ritorno" non è un errore. Ad esempio, è necessario leggere i dati da un socket e quando un blocco di dati è completo, analizzarlo e restituirlo:

class YourBlock { /* block header, format, whatever else */ };

std::optional<YourBlock> cache_and_get_block(
    some_socket_object& socket);

Se i dati allegati hanno completato un blocco analizzabile, è possibile elaborarlo; altrimenti, continua a leggere e ad aggiungere i dati:

void your_client_code(some_socket_object& socket)
{
    char raw_data[1024]; // max 1024 bytes of raw data (for example)
    while(socket.read(raw_data, 1024))
    {
        if(auto block = cache_and_get_block(raw_data))
        {
            // process *block here
            // then return or break
        }
        // else [ no error; just keep reading and appending ]
    }
}

Modifica: riguardo al resto delle tue domande:

Quando è std :: opzionale una buona scelta da usare

  • Quando si calcola un valore e è necessario restituirlo, si ottiene una migliore semantica da restituire per valore piuttosto che prendere un riferimento a un valore di output (che potrebbe non essere generato).

  • Quando si desidera assicurarsi che il codice client debba controllare il valore di output (chiunque scriva il codice client potrebbe non verificare errori - se si tenta di utilizzare un puntatore non inizializzato si ottiene un dump principale; se si tenta di utilizzare un non- inizializzato std :: opzionale, si ottiene un'eccezione intercettabile).

[...] e come compensa ciò che non è stato trovato nel precedente standard (C ++ 11).

Prima di C ++ 11, era necessario utilizzare un'interfaccia diversa per "funzioni che potrebbero non restituire un valore" - o restituire tramite puntatore e controllare NULL, oppure accettare un parametro di output e restituire un codice di errore / risultato per "non disponibile ".

Entrambi impongono uno sforzo e un'attenzione extra da parte dell'implementatore client per farlo bene ed entrambi sono fonte di confusione (il primo spinge l'implementatore client a pensare a un'operazione come un'allocazione e richiede il codice client per implementare la logica di gestione dei puntatori e il secondo codice client per cavarsela usando valori non validi / non inizializzati).

std::optional si prende cura dei problemi che sorgono con le soluzioni precedenti.


So che sono sostanzialmente gli stessi, ma perché stai usando boost::invece di std::?
0x499602D2

4
Grazie - l'ho corretto (l'ho usato boost::optionalperché dopo circa due anni di utilizzo, è codificato nella mia corteccia pre-frontale).
utnapistim,

1
Questo mi sembra un cattivo esempio, dal momento che se fossero stati completati più blocchi solo uno poteva essere restituito e il resto scartato. La funzione dovrebbe invece restituire una raccolta di blocchi possibilmente vuota.
Ben Voigt,

Hai ragione; era un cattivo esempio; Ho modificato il mio esempio per "analizza il primo blocco, se disponibile".
utnapistim,

4

Uso spesso gli optionals per rappresentare i dati opzionali estratti dai file di configurazione, ovvero dove vengono forniti facoltativamente tali dati (come con un elemento previsto, ma non necessario, all'interno di un documento XML), in modo da poter mostrare esplicitamente e chiaramente se i dati erano effettivamente presenti nel documento XML. Soprattutto quando i dati possono avere uno stato "non impostato", rispetto a uno stato "vuoto" e uno "impostato" (logica fuzzy). Con un facoltativo, impostare e non impostare è chiaro, anche vuoto sarebbe chiaro con il valore 0 o null.

Questo può mostrare come il valore di "non impostato" non equivale a "vuoto". In teoria, un puntatore a un int (int * p) può mostrare questo, dove non è impostato un null (p == 0), un valore di 0 (* p == 0) è impostato e vuoto e qualsiasi altro valore (* p <> 0) è impostato su un valore.

Per un esempio pratico, ho un pezzo di geometria estratto da un documento XML che aveva un valore chiamato flag di rendering, in cui la geometria può sovrascrivere i flag di rendering (impostare), disabilitare i flag di rendering (impostare su 0) o semplicemente no influenzare i flag di rendering (non impostati), un optional sarebbe un modo chiaro per rappresentarlo.

Chiaramente un puntatore a un int, in questo esempio, può raggiungere l'obiettivo, o meglio, un puntatore di condivisione in quanto può offrire un'implementazione più pulita, tuttavia, direi che si tratta di chiarezza del codice in questo caso. Un null è sempre un "non impostato"? Con un puntatore, non è chiaro, poiché null significa letteralmente non allocato o creato, anche se potrebbe , ma potrebbe non necessariamente significare "non impostato". Vale la pena sottolineare che un puntatore deve essere rilasciato e, in buona pratica, impostato su 0, tuttavia, come con un puntatore condiviso, un facoltativo non richiede una pulizia esplicita, quindi non c'è alcun problema di confondere la pulizia con l'opzione non è stata impostata.

Credo che si tratti di chiarezza del codice. La chiarezza riduce i costi di manutenzione e sviluppo del codice. Una chiara comprensione dell'intenzione del codice è incredibilmente preziosa.

L'uso di un puntatore per rappresentarlo richiederebbe di sovraccaricare il concetto di puntatore. Per rappresentare "null" come "non impostato", in genere potresti visualizzare uno o più commenti tramite il codice per spiegare questa intenzione. Non è una cattiva soluzione invece di un facoltativo, tuttavia, ho sempre optato per l'implementazione implicita piuttosto che per i commenti espliciti, poiché i commenti non sono esecutivi (come per esempio la compilazione). Esempi di questi elementi impliciti per lo sviluppo (quegli articoli in sviluppo forniti esclusivamente per rafforzare l'intenzione) includono i vari cast in stile C ++, "const" (specialmente sulle funzioni membro) e il tipo "bool", solo per citarne alcuni. Probabilmente non hai davvero bisogno di queste funzionalità di codice, purché tutti obbediscano a intenzioni o commenti.

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.