Jak używać std::visit z wieloma wariantami
std::visit
to potężne narzędzie pozwalające na wywołanie funkcji na aktualnym typie przechowywanym wewnątrz std::variant
. Potrafi znaleźć odpowiednie przeciążenia funkcji, a co więcej, działa dla wielu wariantów naraz.
Przyjrzyjmy się zatem kilku przykładom użycia tej funkcjonalności.
Genialny std::visit
Poniżej mamy klasyczny przykład dla jednego wariantu:
struct Fluid { };
struct LightItem { };
struct HeavyItem { };
struct FragileItem { };
struct VisitPackage
{
void operator()(Fluid& ) { cout << "fluid\n"; }
void operator()(LightItem& ) { cout << "light item\n"; }
void operator()(HeavyItem& ) { cout << "heavy item\n"; }
void operator()(FragileItem& ) { cout << "fragile\n"; }
};
int main()
{
std::variant<Fluid, LightItem, HeavyItem, FragileItem> package { FragileItem() };
std::visit(VisitPackage(), package);
}
Wyjście programu:
fragile
Mamy jeden wariant, który reprezentuje cztery różne rodzaje opakowania, po czym używamy super zaawansowanej struktury VisitPackage
w celu znalezienia jego zawartości. Ten przykład pokazuje nam, że możemy wywołać operację polimorficzną na zestawie klas, o zupełnie innych typach bazowych.
Przypomnienie - zachęcam do przeczytania mojego wpisu wprowadzającego do std::variant
: Everything You Need to Know About std::variant from C++17.
Możemy użyć również “wzorca overload“ w celu użycia kilku wyrażeń lambda:
template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>;
int main()
{
std::variant<Fluid, LightItem, HeavyItem, FragileItem> package;
std::visit(overload{
[](Fluid& ) { cout << "fluid\n"; },
[](LightItem& ) { cout << "light item\n"; },
[](HeavyItem& ) { cout << "heavy item\n"; },
[](FragileItem& ) { cout << "fragile\n"; }
}, package);
}
W powyższym przykładzie nasz kod staje się bardziej przejrzysty, ponieważ nie ma potrzeby deklarowania osobnej struktury przetrzymującej przeładowania dla operatora operator()
.
Przy okazji: czy wiesz, jaka będzie oczekiwana wartość w przykładzie powyżej? Jaka będzie domyślna wartość dla zmiennej package
?
Wsparcie dla wielu wariantów
Co więcej std::visit
współpracuje z wieloma wariantami!
Jeżeli spojrzymy na jego specyfikację, zauważymy że jest on zadeklarowany jako:
template <class Visitor, class... Variants>
constexpr ReturnType visit(Visitor&& vis, Variants&&... vars);
visit
woła funkcję std::invoke
dla wszystkich typów będących w std::variant
:
std::invoke(
std::forward<Visitor>(vis),
std::get<is>(std::forward<Variants>(vars))...
) ;
// gdzie `is...` to `vars.index()...`.
visit
zwraca typ zwracany przez wybrane przeładowanie funkcji.
Przykładowo, możemy wywołać funkcję dla dwóch wariantów przechowywyjących typ opakowania:
std::variant<LightItem, HeavyItem> basicPackA;
std::variant<LightItem, HeavyItem> basicPackB;
std::visit(overload{
[](LightItem&, LightItem& ) { cout << "2 light items\n"; },
[](LightItem&, HeavyItem& ) { cout << "light & heavy items\n"; },
[](HeavyItem&, LightItem& ) { cout << "heavy & light items\n"; },
[](HeavyItem&, HeavyItem& ) { cout << "2 heavy items\n"; },
}, basicPackA, basicPackB);
Po uruchomieniu, ten kod wyświetli nam:
2 light items
Jak widać trzeba dostarczyć funkcje przeciążające dla wszystkich wariacji typów, które mogą wystąpić w funkcji.
Poniżej znajduje się diagram pomocniczy:
Jeżeli mamy dwa warianty -std::variant<A, B, C> abc
oraz std::variant<X, Y, Z> xyz
, to to musimy przygotować zestaw przeładowań dla dziewięciu możliwych wariacji:
func(A, X);
func(A, Y);
func(A, Z);
func(B, X);
func(B, Y);
func(B, Z);
func(C, X);
func(C, Y);
func(C, Z);
W następnej sekcji przyjrzymy się, jak możemy wykorzystać tą funkcjonalność aby dopasować odpowiedni przedmiot do odpowiadającego mu typu opakowania.
Przykład z pakowaniem paczki
std::visit
nie tylko obsługje wiele wariantów, ale również radzi sobie z wariantami klas o różnych typach bazowych.
Spróbuję zilustrować Wam to na podstawie następująco:
Wyobraźmy sobie, że mamy przedmiot (płynny, ciężki, lekki lub delikatny) oraz chcielibyśmy dopasować go do odpowiedniego opakowania (szklanego, kartonowego, wzmocnionego lub odpowiednio zamortyzowanego).
W C++17 z std::variant
oraz std::visit
możemy spróbować zaimplementować to w podobny sposób:
struct Fluid { };
struct LightItem { };
struct HeavyItem { };
struct FragileItem { };
struct GlassBox { };
struct CardboardBox { };
struct ReinforcedBox { };
struct AmortisedBox { };
variant<Fluid, LightItem, HeavyItem, FragileItem> item {
Fluid() };
variant<GlassBox, CardboardBox, ReinforcedBox, AmortisedBox> box {
CardboardBox() };
std::visit(overload{
[](Fluid&, GlassBox& ) {
cout << "fluid in a glass box\n"; },
[](Fluid&, auto ) {
cout << "warning! fluid in a wrong container!\n"; },
[](LightItem&, CardboardBox& ) {
cout << "a light item in a cardboard box\n"; },
[](LightItem&, auto ) {
cout << "a light item can be stored in any type of box, "
"but cardboard is good enough\n"; },
[](HeavyItem&, ReinforcedBox& ) {
cout << "a heavy item in a reinforced box\n"; },
[](HeavyItem&, auto ) {
cout << "warning! a heavy item should be stored "
"in a reinforced box\n"; },
[](FragileItem&, AmortisedBox& ) {
cout << "fragile item in an amortised box\n"; },
[](FragileItem&, auto ) {
cout << "warning! a fragile item should be stored "
"in an amortised box\n"; },
}, item, box);
Kod ten zwróci nam na wyjście:
warning! fluid in a wrong container!
Możecie sami przeanalizować ten przykład na @Coliru.
Mamy zatem cztery typy przedmiotów oraz cztery zestawy opakowań. Chcielibyśmy znaleźć opakowanie do odpowiedniego typu przedmiotu.
std::visit
przyjmuje dwa warianty: item
oraz box
i wywołuje odpowiednie przeciążenie, a następnie sprawdza czy typy się zgadzają lub nie.
Przedstawione przeze mnie typy są bardzo proste, ale nic nie stoi na przeszkodzie, aby rozszerzyć je poprzez dodanie takich właściwości jak waga, rozmiar lub inne.
W teorii powinniśmy dostarczyć funkcji dla wszystkich kombinacji: to znaczy 4*4 = 16 funkcji. Jest jednak sposób na zmniejszenie tej ilości. Możemy dostarczyć tylko 8 niezbędnych funkcji.
Zatem, jak możemy ominąć niektóre z kombinacji?
Jak pominąć niektóre z kombinacji w std::visit
?
Wygląda na to, że możemy użyć lambdy generycznej (C++14), która dostarcza nam możliwość implementacji “domyślnej” funkcji przeciążającej!
Na przykład:
std::variant<int, float, char> v1 { 's' };
std::variant<int, float, char> v2 { 10 };
std::visit(overloaded{
[](int a, int b) { },
[](int a, float b) { },
[](int a, char b) { },
[](float a, int b) { },
[](auto a, auto b) { }, // << domyślna funkcja!
}, v1, v2);
W powyższym przykładzie możecie zauważyć, że użyłem tylko czterech funkcji przeciążających z konkretnymi typami parametrów - nazwijmy je prawidłowymi lub znaczącymi. Reszta obsłużona zostaje przez lambdę generyczną (dostępną w standardzie C++14).
Lambda generyczna zostaje rozwiązana jako funkcja szablonowa. Podczas etapu kompilacji jej priorytet jest niższy niż priorytet dla funkcji posiadających wyszczególnione typy parametrów.
Jeżeli twój visitor został zaimplementowany z użyciem konkretnego typu, to znaczy, że możesz użyć pełnego wsparcia dla labdy generycznej:
template <typename A, typename B>
auto operator()(A, B) { }
Myślę, że ten wzorzec może być przydatny, szczególnie kiedy używacie std::visit
na wariantach dostarczających więcej niż 5 czy 7 przeciążeń, w celu uniknięcia powtórzeń w kodzie.
W naszym głównym przykładzie z przedmiotami i pudełkami ta technika przbierze odrobinę inną postać. Na przykład:
[](FragileItem&, auto ) {
cout << "warning! a fragile item should be stored "
"in an amortised box\n"; },
Generyczna lamda obsłuży wszystkie możliwości dla argumentu typu FragileItem
, gdzie drugi argument staje się nieważny.
Podsumowanie
Ten artykuł pokazał nam jak używać std::visit
dla wielu wariantów. Jest to technika pozwalająca na wprowadzenie algorytmów dostosowywujących odpowiedni wzorzec. Mamy zestaw wielu typów, ale nasz algorytm chcemy wykonać jedynie dla typów odpowiednio dopasowanych. To tak, jakbyśmy chcieli wykonać operacje polimorficzne bez wykorzystania mechanizmu metod wirtualnych.
Jeżeli chcecie dowiedzieć się, jak std::visit
działa pod maską, zachęcam Was do zapoznania się z wpisem Variant Visitation autorstwa Michael Park.
Czy stosowaliście kiedyś std::visit
dla wielu wariantów? Możecie pochwalić się swoimi przykładami?