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.