C++17 hat einen Visitor

Was haben die neuen C++17-Datentypen std::optional, std::any und std::variant gemein? Sie können in-place erzeugt werden. Aber das ist natürlich nicht alles. std::variant unterstützt einen Visitor.

In Pocket speichern vorlesen Druckansicht 9 Kommentare lesen
Lesezeit: 6 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Was haben die neuen C++17-Datentypen std::optional, std::any und std::variant gemein? Sie können in-place erzeugt werden. Aber das ist natürlich nicht alles. std::variant unterstützt einen Visitor.

Aber bevor es mit dem Artikel losgeht, muss ich erst eine Frage klären. Was ist der Job der drei neuen Datentypen?

  • std::optional ist ein Wrapper, der einen oder keinen Wert besitzen kann.
  • std::variant ist eine typsichere Union.
  • std::any ist ein Datentyp, der einen Wert eines beliebigen Typs halten kann.

Natürlich will ich mich nicht selbst wiederholen. In dem Artikel "C++17: Was gibt's Neues in der Bibliothek" stelle ich die drei Datentypen genauer vor. Für die einfache Orientierung zu C++17 gibt's hier die Übersicht.

C++-Zeitache

Was heißt das überhaupt? In-place erzeugen. Der Einfachheit werde ich mich nur auf std::optional beziehen. Ein std::optional<std::string> opt kann einen Wert vom Typ std::string besitzen. In-place bedeutet, dass opt direkt durch die Argumente für std::string erzeugt werden kann.

Ein kleines Beispiel sollte meinen Punkt verdeutlichen.

// inPlace.cpp

#include <optional>
#include <iostream>
#include <string>

int main(){

std::cout << std::endl;

// C string literal
std::optional<std::string> opt1(std::in_place, "C++17"); (1)

// 5 characters 'C'
std::optional<std::string> opt2(std::in_place,5, 'C'); (2)

// initializer list
std::optional<std::string> opt3(std::in_place, {'C', '+', '+', '1', '7'}); (3)

// Copy constructor
std::optional<std::string> opt4(opt3); (4)

std::cout << *opt1 << std::endl;
std::cout << *opt2 << std::endl;
std::cout << *opt3 << std::endl;
std::cout << *opt4 << std::endl;

std::cout << std::endl;

}

opt1 (1), opt2 (2) und opt3 (3) werden mit dem Tag std::in_place erzeugt. Das bedeutet, dass der Konstruktor von std::string direkt mit seinen Argumenten aufgerufen wird. Daher wird der String in-place von einem C-String (1), fünf Buchstaben 'C' (2) und einer Initialisierer-Liste (3) erzeugt. Das gilt aber nicht für opt4 (4). opt4 wird von op3 copy-konstruiert. Hier kommt die Ausgabe des Programms:

Ist in-place Erzeugung neu für dich? Warum? C++ besitzt das Feature schon seit C++11. Die Container der Standard Template Library unterstützen einen ganzen Satz an neuen Methoden, um Elemente hinzuzufügen. Diese Methoden beginnen mit dem Name emplace wie zum emplace_back. Daher kannst du einfach ein neues Element zu einem Container std::vector vec hinzufügen, indem du vec.emplace_back(5) schreibst. Das ist äquivalent zu vec.push_back(int(5)).

Was für ein Zufall. Diese Woche halte ich ein Seminar zu Design Pattern in Python und dann entdecke ich die Funktion std::visit in dem Funktion zu std::variant. Was sich wie das klassische Visitor Pattern anhört, ist tatsächlich ein Visitor für eine Liste von Varianten.

std::visit ermöglicht es, einen Visitor auf eine Liste von Varianten anzuwenden. Der Visitor muss eine aufrufbare Einheit sein. Eine aufrufbare Einheit ist alles, was sich wie eine Funktion anfühlt. Das kann eine Funktion, ein Funktionsobjekt oder eine Lambda-Funktion sein. Um mir das Schreiben einfach zu machen, verwende ich eine Lambda-Funktion.

// visit.cpp

#include <iostream>
#include <vector>
#include <typeinfo>
#include <type_traits>

#include <variant>


int main(){

std::cout << std::endl;

std::vector<std::variant<char, long, float, int, double, long long>> (1)
vecVariant = {5, '2', 5.4, 100ll, 2011l, 3.5f, 2017};

// display each value
for (auto& v: vecVariant){
std::visit([](auto&& arg){std::cout << arg << " ";}, v); (2)
}

std::cout << std::endl;

// display each type
for (auto& v: vecVariant){
std::visit([](auto&& arg){std::cout << typeid(arg).name() << " ";}, v); (3)
}

std::cout << std::endl;

// get the sum
std::common_type<char, long, float, int, double, long long>::type res{}; (4)

std::cout << "typeid(res).name(): " << typeid(res).name() << std::endl;

for (auto& v: vecVariant){
std::visit([&res](auto&& arg){res+= arg;}, v); (5)
}
std::cout << "res: " << res << std::endl;

// double each value
for (auto& v: vecVariant){
std::visit([](auto&& arg){arg *= 2;}, v); (6)
std::visit([](auto&& arg){std::cout << arg << " ";}, v);
}

std::cout << std::endl;

}

In (1) erzeuge ich einen std::vector von Varianten. Jede Variante kann einen char, long, float, int, double oder long long besitzen. Damit geht es schnell von der Hand, den Vektor von Varianten zu traversieren und die Lambda-Funktion auf jedes Element anzuwenden (2). Dank der Funktion typeid gibt jeder Wert seinen Typ zurück. Ich denke, du hast das Visitor Pattern entdeckt: Der Vektor von Varianten ist die besuchte Datenstruktur, auf die ich die visit-Funktion (Visitor) anwende.

Nun gilt es, die Elemente der Varianten zusammenzurechnen. Zuerst benötige ich den richtigen Ergebnistyp zur Übersetzungszeit. std::common_type (4) aus der type-traits-Bibliothek leistet mir wertvolle Dienste. std::common_type gibt mir den Typ zurück, zu denen die Typen char, long, float, int, double und long long automatisch konvertiert werden können. Die abschließenden geschweiften Klammern in res{} bewirken, dass das Ergebnis mit 0.0 initialisiert wird. res ist vom Typ double. (5) berechnet die Summe. Der Visitor kann selbst im Vorbeigehen die Element der Varianten modfizieren. Genau das geschieht in (6).

Hier kommt die Ausgabe des Programms. Mit Visual C++ gibt mir run-time type information mit std::info_type lesbare Namen.

Visual-C++-Ausgabe

Es war nicht einfach, diese Ausgabe zu erzeugen. Um das Programm zu übersetzen, ist ein aktueller GCC Snapshot notwendig. Weder habe ich diesen noch ist dieser online verfügbar. Daher verwendete ich im ersten Schritt den Compiler Explorer auf godbolt, um die Syntax des Programms zu prüfen. Im zweiten Schritt verwende ich den aktuellen Visual C++ Compiler auf webcompiler.cloudapp.net, um das Ergebnis zu erhalten. In diesem Fall ist der Flag std::c++latest notwendig. Zwei von drei Versuchen resultierten in der Fehlermeldung: Maximum execution time exceeded! Aber letztendlich hat es dann funktioniert.

Mit C++17 gibt es parallele Algorithmen der Standard Template Library. Selbst ein paar neue Algorithmen bringt C++17 mit. Welche? Das stelle ich im nächsten Artikel vor. ()