Eine std::variant mit dem Overload Pattern besuchen
Eine std::variant besitzt einen Wert aus einem ihrer Datentypen. std::visit ermöglicht es, einen Besucher auf sie anzuwenden. Hier kommt das praktische Overload Pattern ins Spiel.
Eine std::variant besitzt einen Wert aus einem ihrer Datentypen. std::visit ermöglicht es, einen Besucher auf sie anzuwenden. Hier kommt das praktische Overload Pattern ins Spiel.
![Eine std::variant mit dem Overload Pattern besuchen](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/1/7/3/8/3/2/templates-6f4fbc1ca488102c.png)
In meinem letzten Artikel "Clevere Tricks mit Parameterpacks und Fold Expressions [1]" habe ich das Overload Pattern als einen cleveren Trick zum Erstellen eines Overload Set mit Lambdas vorgestellt. Typischerweise wird das Overload Pattern verwendet, um den Wert einer std::variant
zu besuchen.
Aus meinen C++-Seminaren weiß ich, dass viele Entwicklerinnen und Entwickler std::variant
und std::visit
nicht kennen und stattdessen eine Union verwenden. Deshalb möchte ich std::variant
und std::visit
kurz und kompakt vorstellen.
std::variant
(C++17)
Eine std::variant
ist eine typsichere Union. Eine Instanz von std::variant
besitzt einen Wert einer ihrer Datentypen. Der Wert darf keine Referenz, kein C-array und nicht void
sein. Eine std::variant
kann einen Datentyp mehr als einmal besitzen. Eine default-initialisierte std::variant
wird mit ihrem ersten Datentyp initialisiert. In diesem Fall muss der erste Typ einen Default-Konstruktor anbieten. Hierzu ein auf cppreference.com [2] basierendes Beispiel:
// variant.cpp
#include <variant>
#include <string>
int main(){
std::variant<int, float> v, w;
v = 12; // (1)
int i = std::get<int>(v);
w = std::get<int>(v); // (2)
w = std::get<0>(v); // (3)
w = v; // (4)
// std::get<double>(v); // (5) ERROR
// std::get<3>(v); // (6) ERROR
try{
std::get<float>(w); // (7)
}
catch (std::bad_variant_access&) {}
std::variant<std::string> v("abc"); // (8)
v = "def"; // (9)
}
Ich definiere die beiden Varianten v
und w
. Sie können einen int
- und einen float
-Wert besitzen. Ihr Startwert ist 0. v
erhält den Wert 12 (Zeile 1). std::get<int>(v)
gibt den Wert zurück. In Zeile (2) - (4) siehst du drei Möglichkeiten, der Variante v
die Variante w
zuzuweisen. Dabei musst du einige Regeln beachten. Du kannst den Wert einer Variante aufgrund ihres Datentyps (Zeile 5) oder mit einem Index (Zeile 6) abfragen. Der Typ muss eindeutig und der Index gültig sein. In Zeile (7) enthält die Variante w
einen int
-Wert. Dadurch verursache ich eine std::bad_variant_access
-Exception. Wenn entweder der Konstruktoraufruf oder der Zuweisungsoperator eindeutig ist, findet eine einfache Umwandlung statt. Das ist der Grund dafür, dass eine std::variant<std::string>
in Zeile (8) mit einem C-String konstruiert oder der Variante ein neuer C-String zugewiesen werden kann (Zeile 9).
Natürlich gibt es noch viel mehr über std::variant
zu schreiben. Lies dazu beispielsweise den Artikel "Everything You Need to Know About std::variant from C++17 [3]" von Bartlomiej Filipek.
Dank der Funktion std::visit
bietet C++17 eine praktische Möglichkeit, die Elemente einer std::variant
zu besuchen.
std::visit
Was wie das Besuchsmuster des klassischen Entwurfsmusters klingt, ist in Wirklichkeit eine Art Besucher für die Werte einer Variante.
std::visit
ermöglicht es, einen Besucher auf einen Container mit Varianten anzuwenden. Der Besucher muss ein Callable sein. Ein Callable ist eine Einheit, die du aufrufen kannst. Typische Callables sind Funktionen, Funktionsobjekte oder Lambdas. In meinem Beispiel verwende ich Lambdas:
// visitVariants.cpp
#include <iostream>
#include <vector>
#include <typeinfo>
#include <variant>
int main(){
std::cout << '\n';
std::vector<std::variant<char, long, float, int, double, long long>> // (1)
vecVariant = {5, '2', 5.4, 100ll, 2011l, 3.5f, 2017};
for (auto& v: vecVariant){
std::visit([](auto arg){std::cout << arg << " ";}, v); // (2)
}
std::cout << '\n';
for (auto& v: vecVariant){
std::visit([](auto arg){std::cout << typeid(arg).name() << " ";}, v); // (3)
}
std::cout << "\n\n";
}
In Zeile (1) erstelle ich einen std::vector
von std::variant
s und initialisiere jede Variante. Jede Variante kann einen char-, long-, float-, int-, double-
oder long long
-Wert enthalten. Es ist ganz einfach, den Vektor der Varianten zu durchlaufen und das Lambda auf ihn anzuwenden (siehe Zeile (2) und (3)). Erstens zeige ich den aktuellen Wert an (2), und zweitens erhalte ich dank des Aufrufs typeid(arg).name()
(3) eine String-Darstellung des Datentyps des aktuellen Wertes.
![Eine std::variant mit dem Overload Pattern besuchen](https://heise.cloudimg.io/width/394/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/1/7/3/8/3/2/visitVariants-a1c5503924fa4a8c.png)
Gut? Nein! Ich habe in dem Programm visitVariant.cpp
ein generisches Lambda verwendet. Daher sind die String-Darstellungen der Typen mit dem GCC ziemlich unleserlich: "i c d x l f i
". Ehrlich gesagt, möchte ich auf jeden Typ der Varianten eine spezielle Lambda anwenden. Jetzt kommt das Overload Pattern zum Einsatz.
Overload Pattern
Dank des Overload Patterns kann ich jeden Datentyp mit einem lesbaren String anzeigen und jeden Wert auf eine geeignete Weise darstellen.
// visitVariantsOverloadPattern.cpp
#include <iostream>
#include <vector>
#include <typeinfo>
#include <variant>
#include <string>
template<typename ... Ts> // (7)
struct Overload : Ts ... {
using Ts::operator() ...;
};
template<class... Ts> Overload(Ts...) -> Overload<Ts...>;
int main(){
std::cout << '\n';
std::vector<std::variant<char, long, float, int, double, long long>> // (1)
vecVariant = {5, '2', 5.4, 100ll, 2011l, 3.5f, 2017};
auto TypeOfIntegral = Overload { // (2)
[](char) { return "char"; },
[](int) { return "int"; },
[](unsigned int) { return "unsigned int"; },
[](long int) { return "long int"; },
[](long long int) { return "long long int"; },
[](auto) { return "unknown type"; },
};
for (auto v : vecVariant) { // (3)
std::cout << std::visit(TypeOfIntegral, v) << '\n';
}
std::cout << '\n';
std::vector<std::variant<std::vector<int>, double, std::string>> // (4)
vecVariant2 = { 1.5, std::vector<int>{1, 2, 3, 4, 5}, "Hello "};
auto DisplayMe = Overload { // (5)
[](std::vector<int>& myVec) {
for (auto v: myVec) std::cout << v << " ";
std::cout << '\n';
},
[](auto& arg) { std::cout << arg << '\n';},
};
for (auto v : vecVariant2) { // (6)
std::visit(DisplayMe, v);
}
std::cout << '\n';
}
Zeile (1) erzeugt einen Vektor von Varianten mit ganzzahligen Typen und Zeile (4) einen Vektor von Varianten mit einem std::vector<int>, double
und einem std::string
.
Fahren wir mit der ersten Variante vecVariant
fort. TypeOfIntegral
(2) ist ein Overload Set, das für einige ganzzahlige Typen seine String-Darstellung zurückgibt. Wenn der Typ nicht von dem Overload Set angeboten wird, gebe ich als Fallback den String "unknown type
" zurück. In Zeile (3) wende ich das Overload Set auf jede Variante v
mit std::visit
an.
Die zweite Variante vecVariant2
(4) hat zusammengesetzte Typen. Um ihre Werte darzustellen, erstelle ich ein Overload Set (5). Im Allgemeinen kann ich den Wert einfach in std::cout
ausgeben. Für den std::vector<int>
verwende ich eine range-basierte for
-Schleife (6), um seine Werte darzustellen.
Hier ist die Ausgabe des Programms:
![Eine std::variant mit dem Overload Pattern besuchen](https://heise.cloudimg.io/width/494/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/1/7/3/8/3/2/visitVariantsOverloadPattern-459a2f1e788783e4.png)
Ich möchte noch ein paar Worte zu dem in diesem Beispiel verwendeten Overload Pattern (7) schreiben. Ich habe es bereits in meinem letzten Beitrag "Clevere Tricks mit Parameterpacks und Fold Expressions [4]" vorgestellt.
template<typename ... Ts> // (1)
struct Overload : Ts ... {
using Ts::operator() ...;
};
template<class... Ts> Overload(Ts...) -> Overload<Ts...>; // (2)
Zeile (1) ist das Overload Pattern und Zeile (2) ist der Deduction Guide dafür. Die Struktur Overload kann beliebig viele Basisklassen (Ts ...)
haben. Sie leitet von jeder Klasse public
ab und bringt den Aufrufoperator (Ts::operator...
) jeder Basisklasse in ihren Geltungsbereich. Die Basisklassen benötigen einen überladenen Aufrufoperator (Ts::operator()
). Lambdas stellen diesen Aufrufoperator zur Verfügung. Das folgende Beispiel ist so einfach wie möglich gehalten:
#include <variant>
template<typename ... Ts>
struct Overload : Ts ... {
using Ts::operator() ...;
};
template<class... Ts> Overload(Ts...) -> Overload<Ts...>;
int main(){
std::variant<char, int, float> var = 2017;
auto TypeOfIntegral = Overload { // (1)
[](char) { return "char"; },
[](int) { return "int"; },
[](auto) { return "unknown type"; },
};
}
Wenn du dieses Beispiel in C++ Insights verwendest, wird die Magie erkennbar. Zunächst bewirkt der Aufruf (1) die Erstellung eines vollständig spezialisierten Klassen-Templates.
![Eine std::variant mit dem Overload Pattern besuchen](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/1/7/3/8/3/2/OverloadPatternInstantiated-0bfa1529daaa484c.png)
Zweitens bewirken die verwendeten Lambdas im Overload Pattern wie [](char) { return "char"; }
die Erstellung eines Funktionsobjekts. In diesem Fall gibt der Compiler dem Funktionsobjekt den Namen __lambda_15_9
.
![Eine std::variant mit dem Overload Pattern besuchen](https://heise.cloudimg.io/width/668/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/1/7/3/8/3/2/lambdaChar-ba5a72db9d21b71c.png)
Die Untersuchung der automatisch generierten Datentypen zeigt einen weiteren interessanten Punkt. Der Aufrufoperator von __lambda_15_9
ist für char
überladen: const char * operator() (char) const { return "char"; }.
Der Deduction Guide (template<class... Ts> Overload(Ts...) -> Overload<Ts...>;
) (Zeile 2) wird nur für C++17 benötigt. Dieser teilt dem Compiler mit, wie er aus Konstruktor-Argumenten Template-Parameter erzeugen kann. C++20 kann dieses Klassen-Template automatisch ableiten.
Wie geht es weiter?
Die "Freundschaft" von Templates ist etwas Besonderes. In meinem nächsten Beitrag erkläre ich, warum. ( [5])
URL dieses Artikels:
https://www.heise.de/-6200578
Links in diesem Artikel:
[1] https://www.heise.de/blog/Clevere-Tricks-mit-Parameterpacks-und-Fold-Expressions-6195143.html
[2] http://en.cppreference.com/w/cpp/utility/variant
[3] https://www.cppstories.com/2018/06/variant/
[4] https://www.heise.de/blog/Clevere-Tricks-mit-Parameterpacks-und-Fold-Expressions-6195143.html
[5] mailto:rainer@grimm-jaud.de
Copyright © 2021 Heise Medien