W programowaniu za pomocą języka C++ bardzo często mamy do czynienia z danymi, które mogą, ale nie muszą być dostępne. Oto przykład z programowania gier komputerowych: czy gracz znalazł jakiś przedmiot? Czy w grze pojawił się przeciwnik? Czy dany wskaźnik na obiekt jest ustawiony?
Tradycyjnie w C++ do obsługi takich sytuacji używało się wskaźników, wartości specjalnych lub flag logicznych. Jednak takie podejście jest podatne na błędy i często utrudnia zrozumienie kodu.
Od standardu C++17 mamy do dyspozycji klasę std::optional
, która pozwala elegancko i bezpiecznie przechowywać "opcjonalne" wartości.
std::optional<T>
to szablon klasy wprowadzony w C++17, służący do reprezentowania wartości typu T
, która może, ale nie musi być obecna. To narzędzie pozwala na wyraźne zakomunikowanie, że dana zmienna lub wynik funkcji może nie mieć wartości, zamiast używać wskaźników czy specjalnych wartości „magicznych”.
W praktyce std::optional
działa jak opakowanie lub „pudełko” na obiekt typu T
. Może ono być w stanie:
T
).Dzięki temu możemy bezpiecznie przekazywać informacje o obecności lub braku wartości bez ryzyka błędów związanych z nieprawidłowymi wskaźnikami czy niejednoznacznymi wartościami specjalnymi, które wymagają dodatkowego sprawdzania.
Klasa std::optional<T>
przechowuje w swoim wnętrzu obiekt typu T
w specjalnie zarezerwowanym miejscu pamięci (tzw. buforze) i dodatkową flagę logiczną wskazującą, czy wartość jest obecna (true
) czy nie (false
).
Gdy optional
jest pusty, dostęp do wartości jest niedozwolony i próba jej odczytania skutkuje wyjątkiem std::bad_optional_access
. Dzięki temu programista jest zmuszony jawnie sprawdzać, czy wartość istnieje, zanim z niej skorzysta.
bool operator bool() const
— pozwala na sprawdzenie, czy wartość jest obecna (np. w if).if (opt) { ... }
bool has_value() const
— jawna metoda sprawdzająca obecność wartości.T& operator*() const
— dostęp do wartości (bezpieczny tylko jeśli wartość istnieje).T* operator->() const
— dostęp do wartości przez wskaźnik, ułatwia korzystanie z metod typu T
.T value() const
— zwraca wartość, rzucając wyjątek, jeśli jej brak.T value_or(const T& default_value) const
— zwraca wartość lub wartość domyślną, jeśli brak.void reset()
— usuwa wartość, czyli czyści optional.Załóżmy, że funkcja ma zwrócić wynik obliczenia, ale w pewnych sytuacjach obliczenie nie jest możliwe. Zamiast zwracać magiczną wartość, np. -1 czy nullptr, możemy zwrócić pusty std::optional
. Kod korzystający z funkcji będzie wymuszony na obsłużeniu tego przypadku, co poprawia bezpieczeństwo i czytelność.
auto
, if
z inicjalizatorem, czy structured binding.Dzięki temu std::optional
doskonale sprawdza się w programowaniu gier, gdzie wiele obiektów czy zdarzeń może być nieobecnych lub warunkowo dostępnych.
#include <iostream>
#include <optional>
#include <string>
#include <vector>
struct Item
{
std::string name;
int id;
};
std::optional<Item> findItemById(const std::vector<Item>& inventory, int id)
{
for (const auto& item : inventory)
{
if (item.id == id)
return item; // znaleziono, zwracamy item
}
return std::nullopt; // brak przedmiotu
}
int main()
{
std::vector<Item> inventory {
{ "Miecz", 101 },
{ "Tarcza", 102 },
{ "Mikstura", 103 }
};
auto found = findItemById(inventory, 102);
if (found)
std::cout << "Znaleziono przedmiot: " << found->name << std::endl;
else
std::cout << "Nie znaleziono przedmiotu o podanym ID." << std::endl;
auto notFound = findItemById(inventory, 999);
if (notFound.has_value())
std::cout << "Znaleziono przedmiot: " << notFound->name << std::endl;
else
std::cout << "Nie znaleziono przedmiotu o podanym ID." << std::endl;
return 0;
}
W przykładzie funkcja findItemById
zwraca std::optional<Item>
. Jeśli przedmiot o danym ID istnieje, zwraca go, w przeciwnym wypadku zwraca std::nullopt
, czyli brak wartości.
struct Enemy
{
std::string type;
int health;
};
std::optional<Enemy> getNearbyEnemy(bool enemyVisible)
{
if (enemyVisible)
return Enemy{ "Goblin", 30 };
else
return std::nullopt;
}
int main()
{
auto enemy = getNearbyEnemy(true);
if (enemy)
std::cout << "W pobliżu jest: " << enemy->type << " z " << enemy->health << " punktami życia." << std::endl;
else
std::cout << "Brak przeciwników w pobliżu." << std::endl;
return 0;
}
Do sprawdzania, czy std::optional
zawiera wartość, możemy użyć operatora bool lub metody has_value()
. Dostęp do wartości uzyskujemy przez operator *
lub metodę value()
. Należy jednak pamiętać, że odczyt bez wartości spowoduje wyjątek std::bad_optional_access
.
int getPlayerHealth(std::optional<int> healthOpt)
{
// jeśli healthOpt jest pusty, zwracamy domyślną wartość 100
return healthOpt.value_or(100);
}
int main()
{
std::optional<int> health1 = 80;
std::optional<int> health2;
std::cout << "Życie gracza 1: " << getPlayerHealth(health1) << std::endl; // wypisze 80
std::cout << "Życie gracza 2: " << getPlayerHealth(health2) << std::endl; // wypisze 100
return 0;
}
#include <iostream>
#include <optional>
#include <string>
struct Quest
{
std::string title;
std::string description;
};
class Player
{
std::optional<Quest> currentQuest;
public:
void startQuest(const Quest& q) { currentQuest = q; }
void abandonQuest() { currentQuest.reset(); }
void showQuest() const
{
if (currentQuest)
std::cout << "Aktualna misja: " << currentQuest->title << std::endl;
else
std::cout << "Brak aktualnej misji." << std::endl;
}
};
int main()
{
Player player;
player.showQuest();
player.startQuest({"Ratowanie księżniczki", "Uratuj księżniczkę z zamku"});
player.showQuest();
player.abandonQuest();
player.showQuest();
return 0;
}
Klasa std::optional
jest niezwykle przydatnym narzędziem w nowoczesnym C++, szczególnie w kontekście programowania gier. Pozwala w prosty i bezpieczny sposób reprezentować wartości, które mogą być nieobecne, co poprawia czytelność i bezpieczeństwo kodu.
Zalecam stosować std::optional
wszędzie tam, gdzie wartość może, ale nie musi istnieć, np. w zwracanych wynikach funkcji, stanach obiektów czy konfiguracjach.