Start Kontakt

Klasa std::variant w C++ — typ sumy w programowaniu gier

W programowaniu gier często mamy do czynienia z obiektami lub zdarzeniami, które mogą przyjmować różne, ale ściśle określone typy danych. Przykładowo — zdarzenie może być ruchem, atakiem lub zdobyciem punktów. Tradycyjnie używaliśmy dziedziczenia lub wskaźników, ale to często bywa skomplikowane i nieefektywne.

Od C++17 dostępny jest typ sumy — std::variant. Pozwala on przechowywać dokładnie jedną wartość spośród typów zdefiniowanych w czasie kompilacji. Zapewnia przy tym bezpieczeństwo typów i wygodny mechanizm obsługi.

Co to jest std::variant?

std::variant<T1, T2, ..., Tn> to typ, który może zawierać wartość dokładnie jednego z typów T1 do Tn. W odróżnieniu od klasycznej unii w C, która jest niebezpieczna i nie przechowuje informacji o aktualnym typie wartości, std::variant przechowuje dodatkowo tag (indeks) określający, jaki typ jest aktualnie aktywny. Dzięki temu mamy gwarancję bezpieczeństwa typów i unikamy błędów spowodowanych błędnym interpretowaniem pamięci.

Porównanie ze zwykłą unią (union)

W języku C istnieje konstrukcja union, która pozwala przechowywać różne typy danych w tym samym obszarze pamięci, zajmując tyle miejsca, ile największy z typów. Problemem jest jednak to, że programista musi ręcznie pamiętać, jaki typ jest aktualnie zapisany i unikać niepoprawnego odczytu — brak tu mechanizmu sprawdzania poprawności typów w czasie wykonania.

Przykład unii w C++:

union Data
{
    int i;
    float f;
};

int main()
{
    Data d;
    d.i = 42;
    // Teraz d.f jest niezdefiniowane
    std::cout << d.i << std::endl; // poprawne
    // ale jeśli przypadkowo odczytamy:
    // std::cout << d.f << std::endl; // niezdefiniowane zachowanie!
}

Brak ochrony przed nieprawidłowym odczytem powoduje, że unie są podatne na błędy, zwłaszcza w złożonych programach.

std::variant rozwiązuje ten problem, implementując typ sumy z informacją o aktualnym typie, co gwarantuje bezpieczeństwo typów i wygodne mechanizmy dostępu.

Jak działa std::variant pod maską?

Podstawowe metody i funkcje

Zalety std::variant

Kiedy używać std::variant?

std::variant jest idealny do modelowania zmiennych, które mogą przyjmować jedną z kilku form — na przykład różne rodzaje zdarzeń w grze, stany postaci, różne atrybuty czy wartości zwracane, które mogą być sukcesem, błędem lub innym wynikiem.

W programowaniu gier pomaga upraszczać kod i poprawiać jego bezpieczeństwo, eliminując potrzebę nieczytelnych wskaźników i dynamicznego rzutowania.

Przykład 1: Reprezentacja zdarzenia w grze

#include <iostream>
#include <variant>
#include <string>

struct MoveEvent
{
    int x, y;
};

struct AttackEvent
{
    int damage;
};

struct PickupEvent
{
    std::string itemName;
};

using GameEvent = std::variant<MoveEvent, AttackEvent, PickupEvent>;

void processEvent(const GameEvent& event)
{
    std::visit([](auto&& arg)
    {
        using T = std::decay_t<decltype(arg)>;
        if constexpr (std::is_same_v<T, MoveEvent>)
        {
            std::cout << "Ruch na pozycję: (" << arg.x << ", " << arg.y << ")\n";
        }
        else if constexpr (std::is_same_v<T, AttackEvent>)
        {
            std::cout << "Atak zadający " << arg.damage << " obrażeń\n";
        }
        else if constexpr (std::is_same_v<T, PickupEvent>)
        {
            std::cout << "Podniesiono przedmiot: " << arg.itemName << "\n";
        }
    }, event);
}

int main()
{
    GameEvent e1 = MoveEvent{10, 20};
    GameEvent e2 = AttackEvent{15};
    GameEvent e3 = PickupEvent{"Miecz"};

    processEvent(e1);
    processEvent(e2);
    processEvent(e3);

    return 0;
}

Jak działa std::visit?

Funkcja std::visit przyjmuje funktor (np. lambdę) oraz jeden lub więcej wariantów i wywołuje funktor z aktualną zawartością wariantu. Dzięki temu możemy obsłużyć wszystkie możliwe typy w jednym miejscu, unikając rozbudowanych switch lub if.

Przykład 2: Przechowywanie różnego rodzaju wartości atrybutów postaci

#include <iostream>
#include <variant>
#include <string>
#include <unordered_map>

using AttributeValue = std::variant<int, float, std::string>;

struct CharacterAttributes
{
    std::unordered_map<std::string, AttributeValue> attributes;
};

void printAttribute(const std::string& name, const AttributeValue& value)
{
    std::visit([&](auto&& arg)
    {
        std::cout << name << ": " << arg << std::endl;
    }, value);
}

int main()
{
    CharacterAttributes hero;
    hero.attributes["health"] = 100;
    hero.attributes["mana"] = 45.5f;
    hero.attributes["name"] = std::string("Conan");

    for (const auto& [key, val] : hero.attributes)
    {
        printAttribute(key, val);
    }

    return 0;
}

Podsumowanie

Klasa std::variant to potężne narzędzie do przechowywania wartości jednej spośród kilku różnych typów w sposób bezpieczny i czytelny. W programowaniu gier pomaga tworzyć elastyczne i łatwe w utrzymaniu struktury danych, np. zdarzenia, stany lub atrybuty postaci.

Warto poznać std::variant i wykorzystywać go tam, gdzie potrzebujemy typów sumy, zamiast używać niebezpiecznych unii czy rozbudowanych hierarchii klas.