Wie soll man std :: optional verwenden?

133

Ich lese die Dokumentation von std::experimental::optionalund habe eine gute Vorstellung davon, was es tut, aber ich verstehe nicht, wann ich es verwenden soll oder wie ich es verwenden soll. Die Seite enthält noch keine Beispiele, was es mir schwerer macht, das wahre Konzept dieses Objekts zu verstehen. Wann ist std::optionaleine gute Wahl und wie wird das kompensiert, was im vorherigen Standard (C ++ 11) nicht gefunden wurde?

0x499602D2
quelle
19
Die Boost.Optional-Dokumente könnten etwas Licht ins Dunkel bringen.
Juanchopanza
Scheint, als ob std :: unique_ptr im Allgemeinen dieselben Anwendungsfälle bedienen kann. Ich denke, wenn Sie etwas gegen Neues haben, dann ist optional vielleicht vorzuziehen, aber mir scheint, dass (Entwickler | Apps) mit dieser Art von Sicht auf Neu in einer kleinen Minderheit sind ... AFAICT, optional ist KEINE großartige Idee. Zumindest könnten die meisten von uns bequem ohne sie leben. Persönlich würde ich mich in einer Welt wohler fühlen, in der ich mich nicht zwischen unique_ptr und optional entscheiden muss. Nennen Sie mich verrückt, aber Zen of Python hat Recht: Lassen Sie es einen richtigen Weg geben, etwas zu tun!
Allyourcode
19
Wir möchten nicht immer etwas auf dem Heap zuweisen, das gelöscht werden muss, daher ist kein unique_ptr kein Ersatz für optional.
Krum
5
@allyourcode Kein Zeiger ist ein Ersatz für optional. Stellen Sie sich vor, Sie wollen ein optional<int>oder sogar <char>. Glauben Sie wirklich, dass es "Zen" ist, dynamisch zuzuweisen, zu dereferenzieren und dann zu löschen - etwas, das sonst keine Zuordnung erfordern könnte und kompakt auf den Stapel und möglicherweise sogar in ein Register passt?
underscore_d
1
Ein weiterer Unterschied, der sich zweifellos aus der Zuordnung von Stapel zu Heap des enthaltenen Werts ergibt, besteht darin, dass das Vorlagenargument im Fall von std::optional(solange es sein kann std::unique_ptr) kein unvollständiger Typ sein kann . Genauer gesagt verlangt die Norm, dass T [...] die Anforderungen von Destructible erfüllt .
dan_din_pantelimon

Antworten:

172

Das einfachste Beispiel, das ich mir vorstellen kann:

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

Dasselbe kann stattdessen mit einem Referenzargument erreicht werden (wie in der folgenden Signatur), aber die Verwendung std::optionalmacht die Signatur und Verwendung angenehmer.

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

Eine andere Möglichkeit, dies zu tun, ist besonders schlecht :

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

Dies erfordert eine dynamische Speicherzuweisung, Bedenken hinsichtlich des Eigentums usw. - bevorzugen Sie immer eine der beiden anderen oben genannten Signaturen.


Ein anderes Beispiel:

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

Dies ist extrem vorzuziehen, anstatt std::unique_ptr<std::string>für jede Telefonnummer so etwas wie eine zu haben!std::optionalbietet Ihnen Datenlokalität, was sich positiv auf die Leistung auswirkt.


Ein anderes Beispiel:

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

Wenn die Suche keinen bestimmten Schlüssel enthält, können wir einfach "kein Wert" zurückgeben.

Ich kann es so benutzen:

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

Ein anderes Beispiel:

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

Dies ist viel sinnvoller als beispielsweise vier Funktionsüberladungen, die jede mögliche Kombination von max_count(oder nicht) und min_match_score(oder nicht) erfordern!

Es beseitigt auch den verfluchten "Pass -1für, max_countwenn Sie kein Limit wollen" oder "Pass std::numeric_limits<double>::min()für, min_match_scorewenn Sie keine Mindestpunktzahl wollen"!


Ein anderes Beispiel:

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

Wenn die Abfragezeichenfolge nicht vorhanden ist s, möchte ich "Nein int" - nicht den speziellen Wert, den jemand für diesen Zweck verwendet hat (-1?).


Weitere Beispiele finden Sie in der boost::optional Dokumentation . boost::optionalund std::optionalwird grundsätzlich in Bezug auf Verhalten und Nutzung identisch sein.

Timothy Shields
quelle
13
@gnzlbg std::optional<T>ist nur ein Tund ein bool. Die Implementierungen der Mitgliedsfunktionen sind äußerst einfach. Die Leistung sollte bei der Verwendung eigentlich kein Problem sein - manchmal ist etwas optional. In diesem Fall ist dies häufig das richtige Werkzeug für den Job.
Timothy Shields
8
@ TimothyShields std::optional<T>ist viel komplexer als das. Es verwendet Platzierung newund viele andere Dinge mit der richtigen Ausrichtung und Größe, um es unter anderem zu einem wörtlichen Typ (dh Verwendung mit constexpr) zu machen. Die Naivität Tund boolHerangehensweise würde ziemlich schnell scheitern.
Rapptz
15
@Rapptz Zeile 256: union storage_t { unsigned char dummy_; T value_; ... }Zeile 289: struct optional_base { bool init_; storage_t<T> storage_; ... }Wie ist das nicht "a Tund a bool"? Ich stimme voll und ganz zu, dass die Implementierung sehr knifflig und nicht trivial ist, aber konzeptionell und konkret ist der Typ a Tund a bool. "Die Naivität Tund boolHerangehensweise würde ziemlich schnell scheitern." Wie können Sie diese Aussage machen, wenn Sie sich den Code ansehen?
Timothy Shields
12
@Rapptz speichert immer noch einen Bool und den Platz für einen Int. Die Vereinigung ist nur da, um das optionale nicht ein T zu konstruieren, wenn es nicht wirklich gewollt ist. Es ist immer noch struct{bool,maybe_t<T>}die Gewerkschaft, die einfach da ist, um nicht zu tun, struct{bool,T}was in allen Fällen ein T konstruieren würde.
PeterT
12
@allyourcode Sehr gute Frage. Beide std::unique_ptr<T>und std::optional<T>in gewissem Sinne erfüllen die Rolle eines "optionalen T". Ich würde den Unterschied zwischen ihnen als "Implementierungsdetails" beschreiben: zusätzliche Zuweisungen, Speicherverwaltung, Datenlokalität, Umzugskosten usw. Ich hätte es std::unique_ptr<int> try_parse_int(std::string s);zum Beispiel nie getan , weil dies eine Zuweisung für jeden Anruf verursachen würde, obwohl es keinen Grund dafür gibt . Ich würde niemals eine Klasse mit einem haben std::unique_ptr<double> limit;- warum eine Zuordnung vornehmen und Datenlokalität verlieren?
Timothy Shields
35

Ein Beispiel wird aus dem neu angenommenen Papier zitiert : N3672, std :: optional :

 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
         }
     }
}
taocp
quelle
13
Weil Sie die Informationen darüber weitergeben können, ob Sie eine Aufwärts- oder Abwärts-Anrufhierarchie haben oder nicht, anstatt einen "Phantom" -Wert weiterzugebenint , von dem angenommen wird, dass er eine "Fehler" -Bedeutung hat.
Luis Machuca
1
@Wiz Dies ist eigentlich ein gutes Beispiel. Es (A) lässt str2int()die Konvertierung implementieren, wie es will, (B) unabhängig von der Methode, um die zu erhalten string s, und (C) vermittelt die volle Bedeutung über die optional<int>anstelle einer dummen magischen Zahl, bool/ Referenz oder dynamischen Zuweisungsmethode es tun.
underscore_d
10

aber ich verstehe nicht, wann ich es benutzen soll oder wie ich es benutzen soll.

Überlegen Sie, wann Sie eine API schreiben und ausdrücken möchten, dass der Wert "Keine Rückgabe" kein Fehler ist. Sie müssen beispielsweise Daten aus einem Socket lesen. Wenn ein Datenblock vollständig ist, analysieren Sie ihn und geben ihn zurück:

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

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

Wenn die angehängten Daten einen analysierbaren Block abgeschlossen haben, können Sie ihn verarbeiten. Andernfalls lesen Sie die Daten weiter und hängen Sie sie an:

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 ]
    }
}

Bearbeiten: in Bezug auf den Rest Ihrer Fragen:

Wann ist std :: optional eine gute Wahl

  • Wenn Sie einen Wert berechnen und ihn zurückgeben müssen, ist es besser, die Semantik nach Wert zurückzugeben, als auf einen Ausgabewert zu verweisen (der möglicherweise nicht generiert wird).

  • Wenn Sie diesen Client - Code sicherstellen wollen , hat den Ausgangswert zu überprüfen (wer auch immer den Client - Code schreibt für Fehler kann nicht überprüfen - wenn Sie einen nicht initialisierten Zeiger Sie einen Core Dump erhalten versuchen , zu verwenden; wenn Sie eine unbe verwenden versuchen , initialisiert std :: optional, Sie erhalten eine abfangbare Ausnahme).

[...] und wie kompensiert es das, was im vorherigen Standard (C ++ 11) nicht gefunden wurde.

Vor C ++ 11 mussten Sie eine andere Schnittstelle für "Funktionen, die möglicherweise keinen Wert zurückgeben" verwenden - entweder per Zeiger zurückgeben und auf NULL prüfen oder einen Ausgabeparameter akzeptieren und einen Fehler- / Ergebniscode für "nicht verfügbar" zurückgeben ".

Beide erfordern vom Client-Implementierer zusätzlichen Aufwand und Aufmerksamkeit, um es richtig zu machen, und beide sorgen für Verwirrung (der erste veranlasst den Client-Implementierer, eine Operation als Zuweisung zu betrachten, und erfordert, dass der Client-Code die Zeigerbehandlungslogik implementiert, und der zweite erlaubt Client-Code, um mit ungültigen / nicht initialisierten Werten davonzukommen).

std::optional kümmert sich gut um die Probleme, die bei früheren Lösungen auftreten.

utnapistim
quelle
Ich weiß, dass sie im Grunde gleich sind, aber warum verwenden Sie boost::statt std::?
0x499602D2
4
Danke - ich habe es korrigiert (ich habe es verwendet, boost::optionalweil es nach ungefähr zwei Jahren der Verwendung in meinem präfrontalen Kortex fest codiert ist).
utnapistim
1
Dies scheint mir ein schlechtes Beispiel zu sein, da bei Abschluss mehrerer Blöcke nur einer zurückgegeben und der Rest verworfen werden könnte. Die Funktion sollte stattdessen eine möglicherweise leere Sammlung von Blöcken zurückgeben.
Ben Voigt
Du hast recht; es war ein schlechtes Beispiel; Ich habe mein Beispiel für "ersten Block analysieren, falls verfügbar" geändert.
utnapistim
4

Ich verwende häufig Optionals, um optionale Daten darzustellen, die aus Konfigurationsdateien abgerufen wurden, dh wo diese Daten (z. B. mit einem erwarteten, aber nicht erforderlichen Element in einem XML-Dokument) optional bereitgestellt werden, damit ich explizit und klar zeigen kann, ob Die Daten waren tatsächlich im XML-Dokument vorhanden. Insbesondere wenn die Daten einen "nicht gesetzten" Zustand gegenüber einem "leeren" und einem "gesetzten" Zustand haben können (Fuzzy-Logik). Mit einem optionalen, gesetzt und nicht gesetzt ist klar, wäre auch leer mit dem Wert 0 oder null klar.

Dies kann zeigen, dass der Wert von "nicht gesetzt" nicht gleich "leer" ist. Im Konzept kann ein Zeiger auf ein int (int * p) dies zeigen, wenn keine Null (p == 0) gesetzt ist, ein Wert von 0 (* p == 0) gesetzt und leer ist und jeder andere Wert (* p <> 0) wird auf einen Wert gesetzt.

Als praktisches Beispiel habe ich ein Stück Geometrie aus einem XML-Dokument gezogen, das einen Wert namens Renderflags hatte, wobei die Geometrie entweder die Renderflags überschreiben (setzen), die Renderflags deaktivieren (auf 0 setzen) oder einfach nicht kann Auswirkungen auf die Render-Flags (nicht gesetzt), wäre eine Option eine klare Möglichkeit, dies darzustellen.

Natürlich kann ein Zeiger auf ein int in diesem Beispiel das Ziel erreichen, oder besser ein Freigabezeiger, da er eine sauberere Implementierung bieten kann. Ich würde jedoch argumentieren, dass es in diesem Fall um Codeklarheit geht. Ist eine Null immer ein "nicht gesetzt"? Mit einem Zeiger ist dies nicht klar, da null wörtlich bedeutet, dass es nicht zugewiesen oder erstellt wurde, obwohl dies möglich , aber möglicherweise nicht unbedingt erforderlich ist mean „nicht festgelegt“. Es ist darauf hinzuweisen, dass ein Zeiger freigegeben und in der guten Praxis auf 0 gesetzt werden muss. Wie bei einem gemeinsam genutzten Zeiger erfordert ein optionales Element jedoch keine explizite Bereinigung, sodass keine Bedenken bestehen, die Bereinigung mit zu verwechseln das optionale wurde nicht eingestellt.

Ich glaube, es geht um Code-Klarheit. Klarheit reduziert die Kosten für die Wartung und Entwicklung von Code. Ein klares Verständnis der Code-Absicht ist unglaublich wertvoll.

Die Verwendung eines Zeigers, um dies darzustellen, würde das Überladen des Konzepts des Zeigers erfordern. Um "null" als "nicht gesetzt" darzustellen, werden normalerweise ein oder mehrere Kommentare durch Code angezeigt, um diese Absicht zu erklären. Das ist keine schlechte Lösung anstelle einer optionalen. Ich entscheide mich jedoch immer für eine implizite Implementierung und nicht für explizite Kommentare, da Kommentare nicht durchsetzbar sind (z. B. durch Kompilierung). Beispiele für diese impliziten Elemente für die Entwicklung (jene Artikel in der Entwicklung, die nur zur Durchsetzung von Absichten bereitgestellt werden) sind die verschiedenen C ++ - Casts "const" (insbesondere für Elementfunktionen) und der Typ "bool", um nur einige zu nennen. Wahrscheinlich brauchen Sie diese Codefunktionen nicht wirklich, solange jeder Absichten oder Kommentaren gehorcht.

Kit10
quelle