Szukaj…


Wprowadzenie

Opcjonalne (znane również jako typy Może) służą do reprezentowania typu, którego zawartość może być lub nie być obecna. Są one zaimplementowane w C ++ 17 jako klasa std::optional . Na przykład obiekt typu std::optional<int> może zawierać pewną wartość typu int lub może nie zawierać żadnej wartości.

Opcjonalne są powszechnie używane albo do reprezentowania wartości, która może nie istnieć, albo jako typ zwracany przez funkcję, która może nie zwrócić znaczącego wyniku.

Inne podejścia do opcjonalnych

Istnieje wiele innych podejść do rozwiązania problemu, które std::optional rozwiązuje, ale żadne z nich nie jest całkiem kompletne: użycie wskaźnika, użycie wartownika lub użycie pair<bool, T> .

Opcjonalny vs Wskaźnik

W niektórych przypadkach możemy podać wskaźnik do istniejącego obiektu lub nullptr aby wskazać błąd. Jest to jednak ograniczone do przypadków, w których obiekty już istnieją - optional , jako typ wartości, można również użyć do zwrócenia nowych obiektów bez uciekania się do alokacji pamięci.

Opcjonalne vs Sentinel

Powszechnym idiomem jest używanie specjalnej wartości w celu wskazania, że wartość ta nie ma znaczenia. Może to być 0 lub -1 dla typów całkowych lub nullptr dla wskaźników. Zmniejsza to jednak przestrzeń prawidłowych wartości (nie można rozróżnić między prawidłowym 0 i bez znaczenia 0), a wiele typów nie ma naturalnego wyboru wartości wartownika.

Opcjonalne vs std::pair<bool, T>

Innym powszechnym idiomem jest zapewnienie pary, w której jednym z elementów jest bool wskazujący, czy wartość jest znacząca, czy nie.

Zależy to od tego, że typ wartości jest domyślnie konstruowany w przypadku błędu, co nie jest możliwe w przypadku niektórych typów i możliwe, ale niepożądane w przypadku innych. optional<T> , w przypadku błędu, nie musi niczego konstruować.

Używanie opcji do reprezentowania braku wartości

Przed C ++ 17 posiadanie wskaźników o wartości nullptr zwykle reprezentowało brak wartości. Jest to dobre rozwiązanie dla dużych obiektów, które zostały dynamicznie przydzielone i są już zarządzane przez wskaźniki. Jednak to rozwiązanie nie działa dobrze w przypadku małych lub prymitywnych typów, takich jak int , które rzadko są dynamicznie przydzielane lub zarządzane przez wskaźniki. std::optional zapewnia realne rozwiązanie tego powszechnego problemu.

W tym przykładzie zdefiniowano struct Person . Osoba może mieć zwierzę, ale nie jest to konieczne. Dlatego też członek pet Person jest zadeklarowany za pomocą std::optional opakowania std::optional .

#include <iostream>
#include <optional>
#include <string>

struct Animal {
    std::string name;
};

struct Person {
    std::string name;
    std::optional<Animal> pet;
};

int main() {
    Person person;
    person.name = "John";

    if (person.pet) {
        std::cout << person.name << "'s pet's name is " <<
            person.pet->name << std::endl;
    }
    else {
        std::cout << person.name << " is alone." << std::endl;
    }
}

Używanie opcji do reprezentowania niepowodzenia funkcji

Przed wersją C ++ 17 funkcja zwykle przedstawiała awarię na jeden z kilku sposobów:

  • Zwrócony został wskaźnik zerowy.
    • np. wywołanie funkcji Delegate *App::get_delegate() w instancji App która nie miała delegata, zwróci wartość nullptr .
    • Jest to dobre rozwiązanie dla obiektów, które zostały dynamicznie przydzielone lub są duże i są zarządzane przez wskaźniki, ale nie jest dobrym rozwiązaniem dla małych obiektów, które zwykle są przydzielane do stosu i przekazywane przez kopiowanie.
  • Określona wartość typu zwracanego została zarezerwowana w celu wskazania niepowodzenia.
    • np. wywołanie funkcji unsigned shortest_path_distance(Vertex a, Vertex b) na dwóch nie połączonych wierzchołkach może zwrócić zero, aby wskazać ten fakt.
  • Wartość została sparowana z wartością bool aby wskazać, czy zwracana wartość była znacząca.
    • np Wywołanie funkcji std::pair<int, bool> parse(const std::string &str) z argumentem ciąg, który nie jest liczbą całkowitą wróci parę z nieokreślonej int i bool ustawionym na false .

W tym przykładzie John otrzymuje dwa zwierzaki: Puszysty i Furball. Następnie wywoływana jest funkcja Person::pet_with_name() celu pobrania zwierzaków Johna. Ponieważ John nie ma zwierzaka o nazwie Whiskers, funkcja kończy się niepowodzeniem i zamiast tego zwracane jest std::nullopt .

#include <iostream>
#include <optional>
#include <string>
#include <vector>

struct Animal {
    std::string name;
};

struct Person {
    std::string name;
    std::vector<Animal> pets;
    
    std::optional<Animal> pet_with_name(const std::string &name) {
        for (const Animal &pet : pets) {
            if (pet.name == name) {
                return pet;
            }
        }
        return std::nullopt;
    }
};

int main() {
    Person john;
    john.name = "John";
    
    Animal fluffy;
    fluffy.name = "Fluffy";
    john.pets.push_back(fluffy);
    
    Animal furball;
    furball.name = "Furball";
    john.pets.push_back(furball);
    
    std::optional<Animal> whiskers = john.pet_with_name("Whiskers");
    if (whiskers) {
        std::cout << "John has a pet named Whiskers." << std::endl;
    }
    else {
        std::cout << "Whiskers must not belong to John." << std::endl;
    }
}

opcjonalnie jako wartość zwracana

std::optional<float> divide(float a, float b) {
  if (b!=0.f) return a/b;
  return {};
}

Zwracamy tutaj ułamek a/b , ale jeśli nie jest zdefiniowany (byłby nieskończonością), zwracamy puste opcjonalne.

Bardziej złożony przypadek:

template<class Range, class Pred>
auto find_if( Range&& r, Pred&& p ) {
  using std::begin; using std::end;
  auto b = begin(r), e = end(r);
  auto r = std::find_if(b, e , p );
  using iterator = decltype(r);
  if (r==e)
    return std::optional<iterator>();
  return std::optional<iterator>(r);
}
template<class Range, class T>
auto find( Range&& r, T const& t ) {
  return find_if( std::forward<Range>(r), [&t](auto&& x){return x==t;} );
}

find( some_range, 7 ) przeszukuje kontener lub zasięg some_range poszukiwaniu czegoś równego liczbie 7 . find_if robi to z predykatem.

Zwraca pustą opcjonalną, jeśli nie została znaleziona, lub opcjonalną zawierającą iterator do elementu, jeśli tak było.

Pozwala to na:

if (find( vec, 7 )) {
  // code
}

lub nawet

if (auto oit = find( vec, 7 )) {
  vec.erase(*oit);
}

bez konieczności manipulowania iteratorami i testami rozpoczęcia / zakończenia.

wartość_lub

void print_name( std::ostream& os, std::optional<std::string> const& name ) {
  std::cout "Name is: " << name.value_or("<name missing>") << '\n';
}

value_or zwraca wartość przechowywaną w opcjonalnym lub argument, jeśli nie ma tam nic do zapisania.

Pozwala to wziąć opcjonalną wartość-być może i dać domyślne zachowanie, gdy rzeczywiście potrzebujesz wartości. Robiąc to w ten sposób, decyzja o „domyślnym zachowaniu” może zostać przesunięta z powrotem do punktu, w którym jest najlepiej podjęta i natychmiast potrzebna, zamiast generować pewną domyślną wartość głęboko w trzewiach jakiegoś silnika.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow