C++17: Neuzugänge in den Bibliotheken

Seite 2: Moderne Varianten

Inhaltsverzeichnis

Mit std::variant gibt es einen neuen generischen Datentyp zum Umgang mit festgelegten Varianten wie sie in Unions zu finden sind. Objekte haben jeweils den Wert eines der Datentypen, die Entwickler beim Deklarieren als mögliche Typen angegeben haben. Im Gegensatz zu dem union-Sprachmittel, können sie wirklich jeden beliebigen Datentyp halten, und die Objekte kennen ihren jeweiligen Datentyp. Dafür lässt sich variant nicht zur Typumwandlung von bits verwenden.

Die folgende Deklaration legt ein variant-Objekt an, das einen int und einen String halten kann, und initialisiert das Objekt gleich:

std::variant<int, std::string> var(42);

Fehlt der Wert zur Initialisierung, nutzt die Bibliothek den Default-Konstruktor des ersten Datentyps. Werte anderer Datentypen lassen sich ohne Weiteres zuweisen:

var = "new value";

var.index() gibt den Index des verwendeten Datentyps aus. Mit der obigen Zuweisung wechselt er somit von 0 auf 1.

Der direkte Zugriff ist mit get<>() möglich, was wahlweise die Angabe des korrekten Datentyps oder dessen Index erfordert:

std::string s = std::get<std::string>(var);  // Zugriff ueber Typ
int i = std::get<0>(); // Zugriff ueber Index

Der Compiler versagt das Übersetzen dieser Zeilen bei Verwendung eines Datentyps oder eines Index, der grundsätzlich nicht möglich ist. Implizite Typumwandlungen sind erlaubt, solange sie nicht mehrdeutig sind. Der Zugriff wirft zur Laufzeit eine bad_variant_access-Exception, wenn der Typ des aktuellen Werts nicht passt.

Varianten können den gleichen auch Datentyp mehrfach halten:

std::variant<int, int, std::string> var;  
var.emplace<1>(42); // setzt zweiten int
int i = std::get<1>(); // greift auf zweiten int zu

Eine andere Möglichkeit des Zugriffs sind Visitors. Dabei handelt es sich um Funktionsobjekte (Functors) oder Lambdas, die für jeden Datentyp eine entsprechende Zugriffsfunktion anbieten, wie folgendes Beispiel zeigt:

std::variant<int, std::string> var(42);
struct MyVisitor
{
void operator() (int i) const {
std::cout << i << '\n';
}
void operator() (std::string s) const {
std::cout << s << '\n';
}
};
std::visit(MyVisitor(), var); // ruft passenden operator() auf

Mit den ab C++14 unterstützen generischen Lambdas geht das noch einfacher:

std::visit([](auto&& val) {
std::cout << val << '\n';
},
var);

Mit dem Datentyp std::any steht ein weiterer Mechanismus für mehrere Datentypen zur Verfügung, die jedoch nicht beschränkt sind. Objekte können zur Laufzeit ihren Datentyp im Prinzip beliebig ändern, was die übliche Typbindung zur Laufzeit quasi aufhebt. Zusätzlich können Objekte wirklich leer sein.

Beispielsweise sind folgende Initialisierungen und Zuweisungen möglich:

std::any anyVal;
anyVal = 42;
anyVal = std::string("hello");
anyVal = "oops";

In dem Beispiel ist anyVal erst leer und enthält dann nacheinander einen Integer, einen String und einen Zeiger auf eine Zeichenfolge. Intern erfolgt die Ermittlung und Prüfung des Datentyps über typeid.

Zum Verwenden der enthaltenen Werte ist die Umwandlung des Objekts in den richtigen Wert mit std::any_cast erforderlich:

int i = std::any_cast<int>(anyVal);  

Falls das Objekt keinen int enthält, wird eine bad_any_cast-Exception geworfen. Der Typ muss dabei (bis auf Konstantheit und Referenzen) exakt stimmen. Hält das Objekt einen int, lässt sich der Wert nicht als long oder short auslesen, da sie andere Typ-IDs haben.

Der Typ lässt sich mit type() prüfen:

if (anyVal.type() == typeid(std::string)) ...

Alle Datentypen müssen kopierbar sein. Move-only Datentypen sind nicht erlaubt, die Move-Semantik aber durchaus:

std::string s("a long value disabling SSO");
std::any a;
// move s in to std::any a:
a = std::move(s);
// move out from std::any a to s:
s = std::any_cast<std::string>(std::move(a));

Die Details für C++17 sind aber noch in der Diskussion.