C++
std :: opcjonalne
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 instancjiApp
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.
- np. wywołanie funkcji
- 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.
- np. wywołanie funkcji
- 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ślonejint
ibool
ustawionym nafalse
.
- np Wywołanie funkcji
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.