Hilfstemplates

numeric_limits<>

Einzubindende Header-Datei: limits

Um zur Programmlaufzeit die Eigenschaften von Datentypen zu ermitteln, stellt die Standardbibliothek das Template numeric_limits zur Verfügung.

Da die Member von numeric_limits als statische public-Member definiert sind, kann auch ohne Objektbezug darauf zugegriffen werden. Dabei wird zuerst der Klassenname numeric_limits angegeben und danach in spitzen Klammern der Datentyp, dessen Eigenschaft ermittelt werden soll. Im Anschluss daran folgt der Gültigkeitsbereichsoperator :: und der Name der zu ermittelnden Eigenschaft.

Die nachfolgende Tabelle enthält eine Übersicht über einige Eigenschaften, die mittels numeric_limits ermittelt werden können. Dabei ist zu beachten, dass einige Eigenschaften als Datum vorliegen und andere über Methoden ermittelt werden.

Eigenschaft Bedeutung
bool is_specialized Für den Datentyp gibt es weitere Kenndaten in numeric_limits. Nur wenn diese Eigenschaft true ist, gelten auch die folgenden Kenndaten.
bool is_modulo Operationen auf den Datentyp können zum Überlauf führen. Beispielsweise kann eine Addition von zwei positiven Zahlen als Ergebnis eine negative Zahl zur Folge haben.
bool is_signed true, wenn der Datentyp vorzeichenbehaftet ist, ansonsten false.
is_integer true für alle Integer-Datentypen, false für Gleitkomma- Datentypen
min( ) Liefert den kleinsten Wert des Datentyps zurück.
max( ) Liefert den größten Wert des Datentyps zurück.
epsilon( ) Nur für Gleitkommadaten; liefert den kleinsten von 1.0 abweichenden Wert und ist damit ein Maß für die Genauigkeit des Gleitkommadatums.
#include <print>
#include <limits>

int main()
{
    // maximaler Wert eines ints
    std::println("Maximalwert von int: {}",
                 std::numeric_limits<int>::max());
    // Rundungsfehler eines float- und double-Werts
    std::println("Max. Rundungsfehler bei float: {:4.2}",
                 std::numeric_limits<float>::epsilon());
    std::println("Max. Rundungsfehler bei double: {:4.2}",
                 std::numeric_limits<double>::epsilon());
}

Maximalwert von int: 2147483647
Max. Rundungsfehler bei float: 1.2e-07
Max. Rundungsfehler bei double: 2.2e-16

Type-traits

Einzubindende Header-Datei: type_traits

type-traits stellen zur Compilezeit Informationen über Datentypen zur Verfügung. Sie werden hauptsächlich in Templates eingesetzt, wenn für unterschiedliche Datentypen unterschiedliche Operationen auszuführen sind. Die type-traits-Bibliothek enthält über 100 verschiedene Templates, von denen wir uns nur 3 beispielhaft ansehen. Die übrigen Templates arbeiten in der Regel auf die gleiche Weise.

Alle type-traits-Templates enthalten eine statische bool-Eigenschaft value. Diese Eigenschaft ist true, wenn ein Datentyp das abzuprüfende Merkmal erfüllt. Anstelle nun jedesmal TYPE_TRAIT<T>::value schreiben zu müssen, gibt es von dem meisten type-traits hierfür die Kurzform TYPE_TRAIT<T>_v, d.h., ::value wird durch _v ersetzt.

Sehen wir uns 3 type-traits an.

template <class T> struct is_arithmetic;

value ist true, wenn T ein Integer- oder Gleitkomma-Datentyp ist.

template <class T, class U> struct is_same;

value ist true, wenn T und U denselben Datentyp besitzen.

template <class T> struct is_pointer;

value ist true, wenn T ein Zeiger ist.

Im nachfolgenden Beispiel addiert die Methode ToAdd() nur dann das übergebene Datum zur Template-Eigenschaft, wenn diese ein Integer-Datentyp, Gleitkomma-Datentyp oder ein string ist. Ist die Template-Eigenschaft ein Zeiger, wird eine Meldung ausgegeben.

#include <print>
#include <string>
#include <type_traits>
using namespace std::string_literals;

template <typename T>
class Add
{
    T data;         // beliebige Eigenschaft
public:
    // ctor
    Add(T val): data(val)
    {}
    // Memberfunktion zum Addieren eines Datums
    // zur Eigenschaft
    // Beachte: if constexpr ist ein Compilezeit-Ausdruck!
    void ToAdd(const T val)
    {
        // Wenn T ein Integer- oder Gleitkomma-Datentyp ist
        if constexpr (std::is_arithmetic_v<T>)
            data += val;
        // Wenn T ein std::string Datentyp ist
        if constexpr (std::is_same<std::string,T>::value)
            data.append(val);
        // Wenn T ein Zeiger ist, keine Addition erlaubt
        if constexpr (std::is_pointer_v<T> == true)
            std::println("Keine Zeigeraddition!");
    }
    T GetData() const
    { return data; }
};

int main()
{
    // int-, string- und Zeiger-Objekte definieren
    Add ival{10};
    Add sval{"Guten"s};
    int intVal = 100;
    Add ptr{&intVal};
    // Nun Daten addieren
    ival.ToAdd(20);
    sval.ToAdd(" Morgen"s);
    ptr.ToAdd(&intVal);     // Das geht nicht!
    // Daten ausgeben
    std::println("ival: {}",ival.GetData());
    std::println("sval: {}",sval.GetData());
    std::println("ptr : {}",static_cast<void*>(ptr.GetData()));
}

Keine Zeigeraddition!
ival: 30
sval: Guten Morgen
ptr : 0x39653ffbec

Und wie bereits erwähnt, können type_traits auch in der requires-Anweisung eines Concepts (siehe requires-Anweisung) eingesetzt werden.

Eine Übersicht über alle type_traits erhalten Sie wie üblich auf https://en.cppreference.com unter dem Stichwort type_traits.

bitset

Einzubindende Header-Datei: bitset

Ein bitset dient zum Abspeichern von Bitfolgen, d.h. Folgen von '0' und '1'. Die Anzahl der Bits in einem bitset ist unabhängig von der Anzahl der Bits der intrinsischen Datentypen. So können in einem bitset 21 oder auch 100 Bits abgespeichert werden. Da ein Bit entweder gesetzt ist (1) oder nicht (0), werden bitset-Objekte in der Regel zum Abspeichern von Zuständen eingesetzt.

Definition

Um ein bitset-Objekt zu definieren, wird nach dem Namen des Klassentemplates bitset in spitzen Klammern die Anzahl der Bits n des bitset-Objekts angegeben, gefolgt vom Objektnamen.

std::bitset<n> obj;

Bei einem so definierten bitset sind zu Beginn alle Bits auf 0 gesetzt. Jedoch kann ein bitset bei seiner Definition durch Angabe eines unsigned long long-Wertes, C-Strings oder string-Objekts initialisiert werden.

Es versteht sich von selbst, dass im String nur 0/1 Folgen stehen dürfen. Dabei kann entweder der komplette String (yourFlags im nachfolgende Beispiel) oder nur ein Teilstring (lowFlags) in das bitset übernommen werden. Wird ein Teilstring übernommen, gibt der erste Wert nach dem Stringnamen die Startposition innerhalb des Strings an und der zweite Wert die Anzahl der zu übernehmenden Zeichen.

// bitset mit 8 Bits und dem Wert 7 (00000111)
std::bitset<8> myFlags(7ULL);
// String mit 0/1 Folge, 8 Zeichen
std::string stringFlags("10010011"s);
// String in Bits 'umwandeln'
std::bitset<8> yourFlags(stringFlags);
// niederwertige Flags extrahieren (0011)
std::bitset<4> lowFlags(stringFlags,0,4);
// bitset mit C-String
std::bitset<6> sixBits("001100");

Ein-/Ausgabe eines bitsets

Wird ein bitset ausgegeben, z.B. mit den Stream cout, wird der Inhalt des bitset als 0/1 Folge ausgegeben. Es werden immer alle Bits ausgegeben, d.h. bei einem bitset mit 50 Bits werden auch 50 Bits ausgegeben. Ein bitset kann ebenfalls eingelesen werden, z.B. mit den Stream cin. Die Eingabe darf dabei natürlich nur aus 0/1 Kombinationen bestehen.

Die Bibliotheksfunktionen format() und print() können kein bitset direkt ausgeben. Soll ein bitset ausgegeben werden, ist dieses zuvor mittels der bitset-Methode to_string() in einen String zu konvertieren (siehe nachfolgendes Beispiel).

#include <print>
#include <iostream>
#include <bitset>

int main()
{
    std::bitset<10> bits1{129ULL};
    std::cout << "Mit cout: " << bits1 << '\n';
    std::println("Mit println(): {}",bits1.to_string());
}

Mit cout: 0010000001
Mit println(): 0010000001

bitset Operationen

Das Template bitset definiert eine große Anzahl von Operationen, um die Bits zu bearbeiten. Auch hier werden wir uns nur die Wichtigsten ansehen.

size_t size() const;

Liefert die Anzahl der Bits zurück.

bool any() const;

Liefert true zurück, wenn mindestens ein Bit gesetzt ist.

size_t count() const;

Liefert die Anzahl der gesetzten Bits zurück

bool test (size_t pos) const;

Testet das Bit an Position pos und liefert true zurück, wenn das Bit gesetzt ist.

bitset& set (size_t pos, bool value = true);
bitset& reset (size_t pos);
bitset& flip (size_t pos);

Setzt das Bit an der Position pos auf den Wert value bzw. löscht oder invertiert es. Alle drei Methoden haben weitere Formen, um z.B. alle Bits in einem bitset gleichzeitig zu manipulieren.

bitset& operator[] (std::size_t pos);

Erlaubt den indizierten Schreib-/Lesezugriff auf ein Bit.

std::string to_string() const;
unsigned long to_long() const;
unsigned long long to_ullong() const;

Konvertiert ein bitset in einen string, einen unsigned long oder unsigned long long-Wert.

#include <print>
#include <bitset>
using namespace std::string_literals;

int main()
{
    std::bitset<8> myFlags{"10100011"};
    // Bitset-Daten ausgeben
    std::println("Zu bearbeitende Bitfolge: {}",myFlags.to_string());
    std::println("size:{}, any:{}, count:{}",
          myFlags.size(), myFlags.any(), myFlags.count());

    // Gesetzte Bits ausgeben
    std::print("Gesetzte Bits: ");
    for (size_t i=0; i<myFlags.size(); i++)
        if (myFlags[i])
            std::print("{}, ",i);
    // Alle Bits invertieren
    myFlags.flip();
    // Und ausgeben
    std::println("\nBits invertiert: {}",myFlags.to_string());
}

Zu bearbeitende Bitfolge: 10100011
size:8, any:true, count:4
Gesetzte Bits: 0, 1, 5, 7,
Bits invertiert: 01011100

Damit beenden wir an dieser Stelle die Behandlung des bitset. Wie erwähnt, besitzt das Template eine Reihe weiterer Operatoren, wie z.B. <<= um alle Bits um eine bestimmte Anzahl von Stellen nach links zu schieben.

Übung

scont1_01:

Es ist ein Programm zu erstellen, dass alle Primzahlen im Bereich 0..MAX ermittelt. MAX ist eine im Programm zu definierende Konstante mit einem beliebigen (sinnvollen) Wert.

Zum Abspeichern der Primzahlen ist ein entsprechend großes bitset mit MAX Bits einzusetzen, wobei die Bitposition X der Zahl X entspricht.

Die Primzahlbestimmung erfolgt nach dem Sieb des Eratosthenes, das folgenden Algorithmus verwendet. Beginnend mit der Zahl 2 sind alle Zahlen im Bereich 2...MAX zu streichen, die ohne Rest durch 2 teilbar sind. Danach ist die nächste höhere 'nicht gestrichene' Zahl zu suchen (ergibt nun die Zahl 3), welche auch der nächsten Primzahl entspricht. Ab dieser so gefundenen neuen Primzahl werden dann alle Zahlen gestrichen, die ohne Rest durch diese teilbar sind. Sind die Zahlen gestrichen, beginnt das Spiel wieder von vorne: Suche nach der nächsten nicht gestrichenen Zahl (ergibt jetzt die Zahl 5), streichen der Zahlen die durch diese ohne Rest teilbar sind usw.

Zum Schluss sind alle gefundenen Primzahlen ausgegeben, d.h., es sind die Indizes auszugeben, deren Bit im bitset gesetzt ist.

Gefundene Primzahlen:
2,3,5,7,11,13,17,19,23,29,31,37,41,43,47, 53,59,61,67,71,73,79,83,89,97,

pair

Einzubindende Header-Datei: utility

Die Klasse pair fasst zwei Daten beliebigen Datentyps zusammen. Mithilfe eines pair können z.B. zwei Werte aus einer Methode/Funktion zurückgegeben werden, indem als Returntyp ein pair verwendet wird.

Definition

Um ein pair-Objekt mit intrinsischen Daten zu definieren, sind nach dem Klassennamen pair in spitzen Klammern die beiden Datentypen der Daten anzugeben, die im pair-Objekt abgelegt werden sollen. Im Anschluss daran folgt, wie üblich, der Objektname.

std::pair<DTYP1,DTYP2> obj;

Wird nach dem Objektnamen nichts weiter angegeben, werden beide Daten mit ihrem Standardkonstruktor initialisiert. Zusätzlich stellt pair Konstruktoren bereit, um ein pair-Objekt bei seiner Definition zu initialisieren. Ergeben sich die Datentypen in diesem Fall eindeutig aus den Initialwerten, kann die Angabe der Datentypen DTYP1 und DTYP2 entfallen (siPair und copyPair).

// pair für int und long
std::pair<int, long> emptyPair;
// pair für string und int
std::pair siPair("another text"s, 1);
// Kopieren eines pair-Objekts
std::pair copyPair(siPair);

Außer intrinsische Daten können in einem pair-Objekt Objekte abgelegt werden. Diese Objekte müssen die im Kapitel Objekte und Container aufgeführten Eigenschaften besitzen. Wird ein Objekt in einem pair abgelegt, ist innerhalb der spitzen Klammer des pair die Klasse des Objekts anzugeben.

Die in einem pair abgelegten Daten können beliebig aus Objekten, Objektzeigern, intrinsische Datentypen und Zeigern zusammengesetzt sein. Es ist sogar möglich, als Datentyp ein weiteres pair zu verwenden.

// Leeres pair mit CAny-Objekt und float
std::pair<CAny, float> emptyPair;
// pair mit zwei Window-Objekten
// alternativ: std::pair<Window, Window> winPair(...)
std::pair winPair(Window{...}, Window{});
// pair mit string und Window-Zeiger
std::pair<std::string, Window*>
             swPair("Mein Fenster"s, new Window(...));

Aufpassen müssen Sie, wenn Objektzeiger in einem pair abgelegt werden (swPair im Beispiel). Zum einen wird beim Löschen des pair nicht mehr automatisch das über den Zeiger referenzierte Objekt gelöscht, sondern nur der Objektzeiger. Das Objekt ist also vorher explizit zu löschen.

Und zum anderen wird beim Duplizieren eines solchen pair nur der Zeiger dupliziert. Damit verweisen die Zeiger in beiden pair auf das gleiche Objekt! Das trifft auch dann zu, wenn pair einander zugewiesen werden. Auch hier werden nur die Zeiger zugewiesen. Seien Sie also vorsichtig mit pair die Zeiger enthalten!

Außerdem können mit pair-Objekten Felder gebildet werden. Beachten Sie, dass in diesem Fall immer die Datentypen der im pair abzulegenden Daten bei der Felddefinition mit anzugeben sind.

std::pair<std::string, float> myPairs[]
{
   {"Toast"s,1.60f},
   {"Butter"s,1.05f}
};

Zugriff auf pair-Daten

Auf die im pair abgelegten Daten kann über dessen Eigenschaften first und second zugegriffen werden. first enthält das bei der Definition des pair-Objekts zuerst aufgeführte Datum und second das zweite Datum. Sollen die Daten nur ausgelesen werden, kann dies alternativ auch mittels structured binding erfolgen (Zeile 17).

#include <print>
#include <utility>
using namespace std::string_literals;

int main()
{
    // Definition eines pair-Feldes
    std::pair<std::string, float> myPairs[]
    {
       {"Toast"s,1.60f},
       {"Butter"s,1.05f}
    };
    // Auslesen des ersten pairs
    std::println("Artikel: {}, Preis: {:.2f}",
                 myPairs[0].first,myPairs[0].second);
    // Auslesen des zweiten pairs
    auto [artikel,preis] = myPairs[1];
    std::println("Artikel: {}, Preis: {:.2f}",
                 artikel,preis);
}

Artikel: Toast, Preis: 1.60
Artikel: Butter, Preis: 1.05

pair als Returnwert

Der Datentyp pair lässt sich auch als Returntyp von Methoden/Funktionen einsetzen.

Die nachfolgende Funktion CalcSqrt() dient zur Berechnung der Quadratwurzel. Als Returnwert liefert sie ein pair, in dessen erstem Datum der Erfolg oder Misserfolg der Wurzelberechnung in Form eines bool-Werts abgelegt ist. Konnte die Wurzel erfolgreich berechnet werden, enthält das zweite Datum die Wurzel aus dem übergebenen Wert.

#include <print>
#include <utility>
#include <cmath>
using namespace std::string_literals;
// Funktion zur Berechnung einer Wurzel
// Liefert ein pair zurueck
inline std::pair<bool,double> CalcSqrt(double val)
{
   if (val < 0)
      return std::pair(false,0.0);
   else
      return std::pair(true,std::sqrt(val));
}
int main()
{
    // Wurzeln berechnen lassen
    // und Ergebnis abpruefen
    if (auto result = CalcSqrt(-1.44); !result.first)
       std::println("CalcSqrt() Fehler!");
    else
       std::println("Wurzel: {}",result.second);
    if (auto result = CalcSqrt(1.44); !result.first)
       std::println("CalcSqrt() Fehler!");
    else
       std::println("Wurzel: {}",result.second);
}

CalcSqrt() Fehler!
Wurzel: 1.2

pair-Operatoren

Der Datentyp pair überlädt unter anderem den Zuweisungsoperator. Er weist die beiden Daten first und second einander zu. Sind Objekte in einem pair abgelegt, sollte die Klasse des Objekts den Zuweisungsoperator und den Move-Operator = überladen.

Außer dem Zuweisungsoperator überlädt pair Vergleichsoperatoren. Beim Vergleichen von pair-Objekten hat das Datum first Vorrang. D.h., nur wenn der Vergleich der Daten first true ergibt, werden die Daten second mit in den Vergleich einbezogen. Deshalb ist bei der Definition eines pair-Objekts darauf zu achten, dass die pair-Daten in der richtigen Reihenfolge angegeben werden. Die Reihenfolge ist aber nur dann relevant, wenn pair-Objekte direkt miteinander verglichen werden. Es ist aber immer möglich, die Daten first bzw. second eines pair direkt miteinander zu vergleichen.

tuple

Einzubindende Header-Datei: tuple

Ein tuple erlaubt das Zusammenfassen von beliebig vielen Daten mit beliebigen Datentypen und ist die Verallgemeinerung des pair. Ein tuple wird wie folgt definiert:

std::tuple<DTYP1,DTYP2,....> obj;

Definition

Für die Definition eines tuple gilt sinngemäß das gleiche wie für die Definition eines pairs.

Zugriff auf tuple-Daten und Operationen

Fast alle Operationen die mit einem pair durchgeführt werden können, können ebenfalls mit einem tuple durchgeführt werden. Mit einer Ausnahme: Es gibt aus naheliegenden Gründen keine Eigenschaften first und second um auf ein Datum in einem tuple zuzugreifen. Der Zugriff auf ein tuple-Datum, sowohl lesend wie schreibend, erfolgt u.a. mithilfe des Funktionstemplates

auto ref = std::get<INDEX>(tupObj);

Es liefert eine Referenz auf das Datum an der Position INDEX des tuple-Objekts tupObj zurück, wobei das erste Datum den Index 0 besitzt.

INDEX muss eine zur Compilezeit berechenbare Ganzzahl sein. D.h., es ist nicht möglich, für INDEX z.B. eine Schleifenvariable einzusetzen.

Um die Anzahl der Daten in einem tuple zu ermitteln, wird das Klassentemplate tuple_size verwendet, dessen Eigenschaft value die Anzahl der Daten im tuple enthält.

auto numElements = std::tuple_size<TUP_TYP>::value;

#include <print>
#include <string>
#include <tuple>

using namespace std::string_literals;

// Synonym fuer tuple (Datensatz) definieren
// Datensatz: Flughafenname, Ort, Land, Passagiere/Jahr
using atuple = std::tuple<std::string, std::string,
                          std::string, unsigned long>;

int main()
{
    // tuple-Feld initialisieren
    atuple airports[] {
        {"Hartsfield-Jackson Atlanta International Airport"s,
        "Atlanta"s, "United States"s, 107'394'029UL},
        {"Beijing Capital International Airport"s,
        "Beijing"s, "China"s, 100'983'290UL},
        {"Dubai International Airport"s,
        "Dubai"s, "United Arab Emirates"s,
          89'149'387UL}
    };

    // Inhalt des tuple-Feldes ausgeben
    for (const auto& elem: airports)
    {
        // Einzelzugriff auf tuple-Elemente
        auto name = std::get<0>(elem);
        auto location = std::get<1>(elem);
        auto country = std::get<2>(elem);
        auto pax = std::get<3>(elem);
        std::println("Flughafen: {}\n\tOrt: {}"
                     " in {}\n\tPassgiere:{}",
                     name, location, country, pax);
    }
    // Pax im 1. Element ueberschreiben
    std::get<3>(airports[0]) = 109'000'000UL;
    // Einfacher geht der Zugriff auf die tuple-Elemente
    // mittels structured binding!!
    const auto [name, location, country, pax] = airports[0];
    std::println("Flughafen: {}\n\tOrt: {}"
                 " in {}\n\tPassgiere:{}",
                 name, location, country, pax);
}

Flughafen: Hartsfield-Jackson Atlanta International Airport
    Ort: Atlanta in United States
    Passgiere:107394029
Flughafen: Beijing Capital International Airport
    Ort: Beijing in China
    Passgiere:100983290
Flughafen: Dubai International Airport
    Ort: Dubai in United Arab Emirates
    Passgiere:89149387
Flughafen: Hartsfield-Jackson Atlanta International Airport
    Ort: Atlanta in United States
    Passgiere:109000000

Beachten Sie den Zugriff auf die tuple-Elemente in Zeile 41. Anstelle jedes tuple-Element einzeln auszulesen werden alle Elemente per structured binding auf einmal ausgelesen.

In vielen Fällen kann ein tuple durch ein struct ersetzt werden. Der Vorteil eines tuple ist, dass viele Algorithmen der Standardbibliothek darauf angewandt werden können, da ein tuple standardmäßig vergleichbar ist. Der Nachteil ist allerdings, dass die tuple-Elemente namenlos und daher schwerer lesbar sind. Im Beispiel wäre ein struct einem tuple vorzuziehen.

variant

Einzubindende Header-Datei: variant

Das Klassentemplate variant der Standardbibliothek dient, genauso wie eine union, zum Abspeichern von Daten unterschiedlicher Datentypen auf ein und demselben Speicherplatz. Im Gegensatz zur union erfolgt die Bearbeitung der abgelegten Daten aber typsicher. D.h. wird in einem variant-Objekt ein int-Wert abgelegt, kann der Inhalt des Objekts nur als int-Wert wieder ausgelesen werden.

Definition

Ein variant-Objekt wird wie folgt definiert:

std::variant<DTYP1,DTYP2,....> varObj;

DTYPx definieren die Datentypen, die in der Variante abgelegt werden können, und varObj ist das variant-Objekt. Als Datentypen sind alle bekannten Datentypen zulässig mit Ausnahme von Referenzen, Feldern und dem Datentyp void.

Zugriff auf die Variante

Da variant den Zuweisungsoperator überlädt, kann einem variant-Objekt direkt ein Datum zugewiesen werden. Der Datentyp des Datums muss mit einem in der Variantendefinition aufgeführten Datentypen übereinstimmen.

// Variante definierten
std::variant<int,float> varObj;
// int-Wert in Variante ablegen
varObj = 10;
// Fehler! 1.23 ist vom Typ double
varObj = 1.23;

Für den Lesezugriff auf ein in der Variante abgelegtes Datum ist das Funktionstemplate

auto ref = std::get<DTYP_POS>(varObj);

aufzurufen. Es liefert eine Referenz auf das abgelegte Datum zurück. DTYP_POS ist entweder ein in der variant-Definition aufgeführter Datentyp oder die Position innerhalb der Datentypenliste (0-basierend). Liegt die Position DTYP_POS außerhalb des zulässigen Bereichs oder entspricht der angegebene Datentyp nicht dem Datentyp des aktuell in der Variante abgelegten Datums, wird eine bad_variant_access Ausnahme ausgelöst (Zeile 21 im Beispiel).

#include <print>
#include <variant>

int main()
{
    // Varianten-Definition
    using TheVariant = std::variant<int, float>;
    TheVariant vari;
    // Variante mit int-Wert belegen
    vari = 10;
    // Varianten-Inhalt auslesen
    try
    {
        // Ok, da in Variante ein int-Wert abgelegt ist
        // und der int-Datentyp an erster Stelle steht
        std::println("std::get<0>  : {}",std::get<0>(vari));
        std::println("std::get<int>: {}",std::get<int>(vari));
        // Hier wird eine Ausnahme ausgeloest,da
        // versucht wird, ein float-Datum auszulesen
        // Index 0=int, 1=float
        std::println("std::get<1>  : {}",std::get<1>(vari));
    }
    catch (const std::bad_variant_access& ex)
    {
        std::println("Fehler: {}",ex.what());
    }
}

std::get<0>  : 10
std::get<int>: 10
Fehler: std::get: wrong index for variant

Alternativ kann zum Auslesen der Variante das Funktionstemplate

auto varPtr = std::get_if<DTYP_POS>(&varObj);

verwendet werden. Für den Template-Parameter DTYP_POS gelten die gleichen Bedingungen wie beim Funktionstemplate get<>(). Als Parameter erhält das Funktionstemplate nun die Adresse des auszulesenden Varianten-Objekts und liefert als Rückgabewert, bei gültigem DTYP_POS-Parameter, einen Zeiger auf den Variantenwert. Enthält DTYP_POS einen ungültigen Index oder Datentyp, wird keine Ausnahme ausgelöst, sondern ein NULL Zeiger zurückgeliefert.

Die Zuweisung

auto var = varObj;

liest nicht den Inhalt der Variante aus, sondern erzeugt eine Varianten-Kopie.

variant-Operationen

Variantenobjekte können mit den üblichen Vergleichsoperatoren verglichen werden, wobei die Variantenobjekte vom gleichen Typ sein müssen. Beim Vergleich werden zuerst die Variantenindizes, also indirekt die in den Variantenobjekten abgelegten Datentypen, verglichen. Und nur wenn die Variantenindizes übereinstimmen, werden die Daten anschließend verglichen.

Zum Ermitteln der Anzahl der bei einer Variantendefinition aufgelisteten Datentypen dient das Template

size_t len = std::variant<VAR>::value;

VAR ist hierbei der Variantentyp. Soll die Anzahl der Datentypen eines Variantenobjekts ermittelt werden, kann für VAR der Spezifizierer decltype(varObj) verwendet werden.

Das Template

bool res = std::holds_alternative<DTYP>(varObj);

prüft, ob im Variantenobjekt varObj ein Datum vom Typ DTYP abgelegt ist.

#include <print>
#include <variant>
int main()
{
    // Varianten-Definition
    using TheVariant = std::variant<int, float>;
    // Zwei Varianten mit ints initialisieren
    TheVariant var1 = 10;
    TheVariant var2 = 11;
    // Dritte Variante mit float initialisieren
    TheVariant var3 = 1.0f;
    // int Varianten vergleichen, ergibt true
    if (var1 < var2)
        std::println("var1 < var2");
    else
        std::println("var1 >= var2");
    // int und float Variante vergleichen,
    // ergibt true wegen Index(int) < Index(float)
    if (var1 < var3)
        std::println("var1 < var3");
    else
        std::println("var1 >= var3");
    // Prueft ob im Variantenobjekt aktuell
    // ein float-Datum abgelegt ist
    if (std::holds_alternative<float>(var1))
        std::println("var1 enthaelt einen float-Wert");
    else
        std::println("var1 enthaelt keinen float-Wert");
}

var1 < var2
var1 < var3
var1 enthaelt keinen float-Wert

variant und Visitor

Im Zusammenspiel mit variant sei an dieser Stelle auf ein weiteres Funktionstemplate visit() hingewiesen. Mithilfe dieses Funktionstemplates können in Abhängigkeit von dem in der Variante abgelegten Datentyp unterschiedliche Aktionen ausgeführt werden.

std::visit(callObj, varObj);

callObj ist ein aufrufbares Objekt (in der Regel ein Objekt, welches den Operator () überlädt) und varObj das Variantenobjekt. Durch mehrfaches Überladen des Operators () für die unterschiedlichen Datentypen in der Variante können auf diese Weise verschiedene Aktionen je nach abgelegtem Datentyp ausgeführt werden. Das nachfolgende Beispiel gibt die im Variantenfeld abgelegten Datentypen aus.

#include <print>
#include <variant>
#include <string>

// Visitor-Struktur
struct ProcessVariant
{
    void operator()(int& val) const
    {
        std::println("int Wert");
    }
    void operator() (float& val) const
    {
        std::println("float Wert");
    }
};

int main()
{
    // Varianten-Definition
    using TheVariant = std::variant<int, float>;
    // Varianten-Feld definieren und initialisieren
    TheVariant vArray[] {1, 2.2f, 3.3f, 4};
    // Datentypen der abgelegten Daten ausgeben
    for(auto element: vArray)
        std::visit(ProcessVariant(),element);
}

int Wert
float Wert
float Wert
int Wert

Anstelle einer Klasse ProcessVariant kann auch ein entsprechender Lambda-Ausdruck verwendet werden.

auto ProcessVariant = [](auto val)
{
    if constexpr (std::same_as<decltype(val),int>)
        std::println("int Wert");
    else
        if constexpr (std::same_as<decltype(val),float>)
            std::println("float Wert");
};

Übungen

htemp_01:

Aus der Datei swr1_hitparade.csv sind der Titel, der Interpret und die Hitparadenposition im ersten Jahr (also 1989) einzulesen. Der Aufbau der Datei ist im Kapitel String I, Übungen beschrieben.

Lesen Sie die ersten 5 Einträge ein die im Jahr 1989 in der Hitparade vertreten waren.

Titel, Interpret und Position sind in einem entsprechenden tuple abzulegen und auszugeben.

Die eingelesenen Daten sind aufsteigend nach der Position zu sortieren (Reihenfolge der Daten im tuple beachten!).

Geben Sie die sortierten Einträge aus.

P:394, T:1999, A:prince
P:813, T:10. Jun, A:bap
P:178, T:1000 gute gruende, A:toten hosen
P:466, T:a forest, A:cure
P:675, T:a question of lust, A:depeche mode
------------------------
P:178, T:1000 gute gruende, A:toten hosen
P:394, T:1999, A:prince
P:466, T:a forest, A:cure
P:675, T:a question of lust, A:depeche mode
P:813, T:10. Jun, A:bap

htemp_02:

Schreiben Sie ein Programm zum Verwalten eines Aktiendepots.

Ein Depoteintrag besteht aus einem Aktiennamen (string-Datentyp) und dem Aktienkurs (float-Wert). Die Daten sind in einem pair-Feld abzulegen.

Initialisieren das pair-Feld mit folgenden Werten:

Daimer 47.60f
VW 141.88f
BMW 62.30f
General Motors 31.18f

Das pair-Feld ist so anzulegen, dass die Aktienkurse auf einfache Weise verglichen werden können.

Erhöhen Sie danach den Kurs der VW-Aktie um 5.00 und sortieren Sie die Aktienkurse aufsteigend nach ihrem Kurswert. Geben Sie zur Kontrolle alle Aktienkurse aus.

Aktienkurse:
-----------:
Aktie: Daimler, Kurs: 47.60
Aktie: VW, Kurs: 141.88
Aktie: BMW, Kurs: 62.30
Aktie: General Motors, Kurs: 31.18

** VW Aktie um 5.00 erhoeht!**
Aktien nach Kurswert sortiert:
-------------------------
Aktie: General Motors, Kurs: 31.18
Aktie: Daimler, Kurs: 47.60
Aktie: BMW, Kurs: 62.30 Aktie:
VW, Kurs: 146.88