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.
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.
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.
std::variant
rezerwuje miejsce na największy spośród typów, które może przechowywać.std::get<T>(variant)
— zwraca referencję do wartości typu T
, jeśli aktualnie jest przechowywany, inaczej rzuca wyjątek std::bad_variant_access
.std::get<index>(variant)
— zwraca referencję do wartości pod typem o indeksie index
.std::get_if<T>(&variant)
— zwraca wskaźnik do wartości typu T
lub nullptr jeśli typ jest inny.std::visit(visitor, variant)
— wywołuje funktor (np. lambda) z aktualną wartością wariantu, pozwalając na elegancką obsługę wszystkich przypadków.index()
— zwraca aktualny indeks typu przechowywanego w wariancie.std::visit
zamiast if
/switch
i rzutowania.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.
#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;
}
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
.
#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;
}
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.