Jak należy używać std :: optional?

140

Czytam dokumentację programu std::experimental::optionali mam dobre pojęcie o tym, co robi, ale nie rozumiem, kiedy powinienem go używać lub jak powinienem go używać. Strona nie zawiera jeszcze przykładów, co utrudnia mi zrozumienie prawdziwej koncepcji tego obiektu. Kiedy jest std::optionaldobrym wyborem do użycia i jak kompensuje to, czego nie znaleziono w poprzednim standardzie (C ++ 11).

0x499602D2
źródło
19
Dopalacz . Opcjonalne dokumenty mogą rzucić trochę światła.
juanchopanza
Wygląda na to, że std :: unique_ptr może generalnie obsługiwać te same przypadki użycia. Myślę, że jeśli masz coś przeciwko nowemu, to opcjonalne może być lepsze, ale wydaje mi się, że (programiści | aplikacje) z takim poglądem na nowe są w niewielkiej mniejszości ... OCZEKIWANIE, opcjonalne NIE jest dobrym pomysłem. Przynajmniej większość z nas mogłaby bez niego wygodnie żyć. Osobiście czułbym się bardziej komfortowo w świecie, w którym nie muszę wybierać między unique_ptr a opcjonalnym. Nazwijcie mnie szalonym, ale Zen of Python ma rację: niech będzie jeden właściwy sposób na zrobienie czegoś!
allyourcode
19
Nie zawsze chcemy alokować na stercie coś, co ma zostać usunięte, więc żaden unique_ptr nie zastąpi opcjonalnego.
Krum
5
@allyourcode Żaden wskaźnik nie zastępuje optional. Wyobraź sobie, że chcesz, optional<int>a nawet <char>. Czy naprawdę myślisz, że to „Zen” wymaga dynamicznego przydzielania, wyłuskiwania, a następnie usuwania - coś, co w innym przypadku nie wymagałoby alokacji i mieściło się w stosie, a może nawet w rejestrze?
underscore_d
1
Inną różnicą, niewątpliwie wynikającą z alokacji stosu vs sterty zawartej wartości, jest to, że argument szablonu nie może być niekompletnym typem w przypadku std::optional(chociaż może być for std::unique_ptr). Dokładniej, norma wymaga, aby T [...] spełniało wymagania Zniszczalności .
dan_din_pantelimon

Odpowiedzi:

175

Najprostszy przykład jaki przychodzi mi do głowy:

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

To samo można osiągnąć za pomocą argumentu referencyjnego (jak w poniższej sygnaturze), ale użycie std::optionalsprawia, że ​​podpis i użycie są przyjemniejsze.

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

Inny sposób, w jaki można to zrobić, jest szczególnie zły :

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

Wymaga to dynamicznej alokacji pamięci, martwienia się o własność itp. - zawsze preferuj jeden z dwóch pozostałych podpisów powyżej.


Inny przykład:

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

Jest to bardzo korzystne rozwiązanie zamiast mieć coś takiego jak „a” std::unique_ptr<std::string>dla każdego numeru telefonu! std::optionalzapewnia lokalizację danych, która jest świetna do wydajności.


Inny przykład:

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

Jeśli wyszukiwanie nie zawiera określonego klucza, możemy po prostu zwrócić „brak wartości”.

Mogę to wykorzystać w ten sposób:

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

Inny przykład:

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

Ma to o wiele więcej sensu niż, powiedzmy, cztery przeciążenia funkcji, które przyjmują każdą możliwą kombinację max_count(lub nie) i min_match_score(lub nie)!

To również eliminuje ten przeklęty „pass -1na max_countjeśli nie chcesz limit” lub „pass std::numeric_limits<double>::min()na min_match_scorejeśli nie chcesz wynik minimum”!


Inny przykład:

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

Jeśli ciągu zapytania nie ma s, chcę "nie int" - nie jakiejkolwiek specjalnej wartości, którą ktoś zdecydował się użyć w tym celu (-1?).


Dodatkowe przykłady można znaleźć w boost::optional dokumentacji . boost::optionali std::optionalbędą zasadniczo identyczne pod względem zachowania i użycia.

Timothy Shields
źródło
13
@gnzlbg std::optional<T>to tylko a Ti a bool. Implementacje funkcji składowych są niezwykle proste. Wydajność nie powinna być istotna podczas korzystania z niego - są chwile, kiedy coś jest opcjonalne, w takim przypadku często jest to właściwe narzędzie do pracy.
Timothy Shields
8
@TimothyShields std::optional<T>jest o wiele bardziej skomplikowane. Używa umieszczania newi wielu innych rzeczy z odpowiednim wyrównaniem i rozmiarem, aby constexprmiędzy innymi uczynić go typem dosłownym (tj. Używaj z ). Naiwność Ti boolpodejście szybko by się nie udały.
Rapptz
16
@Rapptz Linia 256: union storage_t { unsigned char dummy_; T value_; ... }Linia 289: struct optional_base { bool init_; storage_t<T> storage_; ... }Dlaczego to nie jest „a Ti a bool”? Całkowicie się zgadzam, że implementacja jest bardzo skomplikowana i nietrywialna, ale koncepcyjnie i konkretnie typ to a Ti a bool. „Naiwność Ti boolpodejście szybko by się nie udały”. Jak możesz to stwierdzić, patrząc na kod?
Timothy Shields
13
@Rapptz nadal przechowuje bool i miejsce na int. Związek istnieje tylko po to, aby opcjonalny nie konstruował T, jeśli nie jest faktycznie potrzebny. Nadal jest struct{bool,maybe_t<T>}to związek, którego po prostu nie można zrobić, struct{bool,T}co stworzyłoby T we wszystkich przypadkach.
PeterT
13
@allyourcode Bardzo dobre pytanie. Jedno std::unique_ptr<T>i std::optional<T>drugie w pewnym sensie spełnia rolę „opcjonalnego T”. Opisałbym różnicę między nimi jako "szczegóły implementacji": dodatkowe alokacje, zarządzanie pamięcią, lokalność danych, koszt przeniesienia itp. Nigdy bym tego nie zrobił, std::unique_ptr<int> try_parse_int(std::string s);na przykład, ponieważ spowodowałoby to alokację dla każdego połączenia, nawet jeśli nie ma powodu, aby . Nigdy nie miałbym klasy z std::unique_ptr<double> limit;- po co przydzielać i tracić lokalność danych?
Timothy Shields
35

Przykład jest cytowany z nowego przyjętego artykułu: 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
źródło
13
Ponieważ możesz przekazać informacje o tym, czy masz inthierarchię wywołań w górę lub w dół, zamiast przekazywać jakąś „fantomową” wartość, która została „przyjęta” jako „błąd”.
Luis Machuca
1
@Wiz To właściwie świetny przykład. To (A) pozwala str2int()zaimplementować konwersję w dowolny sposób, (B) niezależnie od metody uzyskania string s, a (C) przekazuje pełne znaczenie poprzez optional<int>zamiast jakiejś głupiej magicznej liczby, bool/ referencji lub sposobu opartego na alokacji dynamicznej robić to.
underscore_d
10

ale nie rozumiem, kiedy powinienem go używać lub jak powinienem go używać.

Zastanów się, kiedy piszesz API i chcesz wyrazić, że „brak wartości zwracanej” nie jest błędem. Na przykład musisz odczytać dane z gniazda, a gdy blok danych jest kompletny, analizujesz go i zwracasz:

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

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

Jeśli dołączone dane wypełniły blok parsowalny, możesz go przetworzyć; w przeciwnym razie czytaj i dołączaj dane:

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

Edycja: w odniesieniu do pozostałych pytań:

Kiedy jest std :: optional, jest to dobry wybór

  • Kiedy obliczasz wartość i musisz ją zwrócić, lepsza semantyka zwraca wartość niż branie odniesienia do wartości wyjściowej (która może nie zostać wygenerowana).

  • Jeśli chcesz mieć pewność, że kod klienta musi sprawdzić wartość wyjściową (ktokolwiek pisze kod klienta, może nie sprawdzić pod kątem błędów - jeśli spróbujesz użyć niezainicjowanego wskaźnika, otrzymasz zrzut rdzenia; jeśli spróbujesz użyć un- zainicjalizowany std :: optional, otrzymasz wyjątek, który można złapać).

[…] i jak kompensuje to, czego nie znaleziono w poprzednim standardzie (C ++ 11).

Przed C ++ 11 trzeba było używać innego interfejsu dla „funkcji, które mogą nie zwracać wartości” - albo zwracać wskaźnik i sprawdzać, czy nie ma wartości NULL, albo akceptować parametr wyjściowy i zwracać kod błędu / wyniku dla „niedostępne” ”.

Oba wymagają dodatkowego wysiłku i uwagi ze strony osoby wdrażającej klienta, aby zrobić to dobrze, i oba są źródłem zamieszania (pierwsze zmuszają osobę wdrażającą klienta do myślenia o operacji jako alokacji i wymagają od kodu klienta implementacji logiki obsługi wskaźników, a druga kod klienta, aby uniknąć używania nieprawidłowych / niezainicjowanych wartości).

std::optional ładnie rozwiązuje problemy wynikające z wcześniejszych rozwiązań.

utnapistim
źródło
Wiem, że są w zasadzie takie same, ale dlaczego używasz boost::zamiast std::?
0x499602D2
4
Dzięki - poprawiłem go (użyłem, boost::optionalponieważ po około dwóch latach używania jest zakodowany na stałe w mojej korze przedczołowej).
utnapistim
1
Wydaje mi się, że jest to kiepski przykład, ponieważ jeśli ukończono wiele bloków, tylko jeden można było zwrócić, a pozostałe wyrzucić. Funkcja powinna zamiast tego zwrócić możliwie pustą kolekcję bloków.
Ben Voigt
Masz rację; to był kiepski przykład; Zmodyfikowałem mój przykład na „przeanalizuj pierwszy blok, jeśli jest dostępny”.
utnapistim
4

Często używam opcji opcjonalnych do reprezentowania opcjonalnych danych pobranych z plików konfiguracyjnych, to znaczy, gdzie te dane (takie jak oczekiwany, ale niepotrzebny element w dokumencie XML) są opcjonalnie dostarczane, więc mogę wyraźnie i wyraźnie pokazać, czy dane faktycznie były obecne w dokumencie XML. Zwłaszcza, gdy dane mogą mieć stan „nieustawiony” w przeciwieństwie do stanu „pusty” i „ustawiony” (logika rozmyta). Z opcjonalnym ustawieniem i nieustawieniem jest jasne, również puste byłoby jasne z wartością 0 lub null.

Może to pokazać, że wartość „nie ustawiono” nie jest równoważna wartości „pusty”. W ujęciu koncepcyjnym wskaźnik do int (int * p) może to pokazać, gdy null (p == 0) nie jest ustawiony, wartość 0 (* p == 0) jest ustawiona i pusta, a każda inna wartość (* p <> 0) jest ustawione na wartość.

Dla praktycznego przykładu mam kawałek geometrii wyciągnięty z dokumentu XML, który miał wartość zwaną flagami renderowania, gdzie geometria może albo nadpisać flagi renderowania (ustawione), wyłączyć flagi renderowania (ustawione na 0), albo po prostu nie wpływają na flagi renderowania (nieustawione), opcjonalny byłby wyraźnym sposobem przedstawienia tego.

Oczywiście wskaźnik do int, w tym przykładzie, może osiągnąć cel lub, lepiej, wskaźnik udziału, ponieważ może zaoferować czystszą implementację, jednak twierdzę, że w tym przypadku chodzi o przejrzystość kodu. Czy wartość null zawsze oznacza „nieustawione”? W przypadku wskaźnika nie jest to jasne, ponieważ null dosłownie oznacza, że ​​nie został przydzielony lub utworzony, chociaż może , ale niekoniecznie musi oznaczać „nieustawiony”. Warto zwrócić uwagę, że wskaźnik musi zostać zwolniony i zgodnie z dobrą praktyką ustawiony na 0, jednak podobnie jak w przypadku wskaźnika współdzielonego opcja opcjonalna nie wymaga jawnego czyszczenia, więc nie ma obawy o mylenie czyszczenia z opcja nie została ustawiona.

Uważam, że chodzi o przejrzystość kodu. Clarity obniża koszty utrzymania i rozwoju kodu. Jasne zrozumienie intencji kodu jest niezwykle cenne.

Użycie wskaźnika do reprezentacji tego wymagałoby przeciążenia koncepcji wskaźnika. Aby przedstawić „null” jako „nieustawione”, zazwyczaj można zobaczyć w kodzie jeden lub więcej komentarzy wyjaśniających ten zamiar. To nie jest złe rozwiązanie zamiast opcjonalnego, jednak zawsze wybieram niejawną implementację zamiast jawnych komentarzy, ponieważ komentarze nie są egzekwowalne (na przykład przez kompilację). Przykłady tych niejawnych elementów do programowania (te artykuły w fazie rozwoju, które są dostarczane wyłącznie w celu wymuszenia intencji) obejmują różne rzutowania w stylu C ++, „const” (zwłaszcza w funkcjach składowych) i typ „bool”, żeby wymienić tylko kilka. Prawdopodobnie nie potrzebujesz tych funkcji kodu, o ile wszyscy przestrzegają intencji lub komentarzy.

Zestaw 10
źródło