Start Kontakt

Klasa std::optional w C++ — wartości opcjonalne

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.

Co to jest std::optional?

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:

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.

Jak działa pod maską?

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.

Podstawowe metody i operatory

Przykład semantyczny

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ść.

Zalety std::optional

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.

Przykład 1: Znajdowanie przedmiotu w ekwipunku

#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.

Przykład 2: Sprawdzanie obecności przeciwnika

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

Sprawdzanie i dostęp do wartości

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.

Przykład 3: Ustawianie punktów życia gracza lub wartości domyślnej

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

Przykład 4: Przechowywanie aktualnej misji lub jej braku

#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;
}

Podsumowanie

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.