std::variant wurde in C++17 als typensichere Union-Alternative eingeführt, die entwickelt wurde, um die fehleranfälligen und manuell verwalteten C-Stil-Unionen zu ersetzen. Es wird die Invarianz durchgesetzt, dass es immer genau einen seiner angegebenen alternativen Typen hält, was zur Kompilierzeit Typensicherheit und intuitive Wertsemantik bietet. Dieses Design garantiert theoretisch, dass Operationen wie std::visit oder std::get immer einen gültigen Typ haben, mit dem sie arbeiten können.
Der Zustand valueless_by_exception stellt einen bestimmten Fehlermodus dar, in dem die Variante keinen Wert hält, da während typwechselnder Operationen eine Ausnahme aufgetreten ist. Diese Situation tritt auf, wenn die Variante ihren aktuellen Alternativen zerstören muss, um Platz für eine neue zu schaffen, aber die anschließende Konstruktion der neuen Alternative eine Ausnahme auslöst. Folglich bleibt das Objekt ohne ein gültiges aktives Mitglied zurück, was die Standardinvarianz der Variante vorübergehend verletzt.
Die Lösung, die der Standard bietet, besteht darin, diesen einzigen ungültigen Zustand zuzulassen, um grundlegende Ausnahmesicherheitsgarantien aufrechtzuerhalten. In diesem Zustand bleibt die Variante zerstörbar und zuweisbar, was es ermöglicht, Ressourcen zu bereinigen und neue Werte im Speicher zu platzieren. Um sich vollständig von diesem Zustand zu erholen, muss man erfolgreich einen neuen Wert zuweisen oder einfügen, was die Invarianz wiederherstellt, indem eine gültige Alternative geschaffen und der interne Status zurückgesetzt wird.
std::variant<std::string, int> v = "hello"; try { v.emplace<std::string>(10000000, 'x'); // könnte bad_alloc werfen } catch (...) { assert(v.valueless_by_exception()); v = 42; // Wiederherstellung: wieder gültig }
Betrachten Sie ein Hochfrequenz-Handelssystem, das Marktdaten-Nachrichten verarbeitet, die als std::variant<PriceUpdate, OrderCancel, TradeExecution> dargestellt werden. In einer speicherbeschränkten Situation führt ein Versuch, ein großes TradeExecution-Objekt zuzuweisen, zu std::bad_alloc, nachdem die Variante bereits das vorherige PriceUpdate zur Schaffung von Platz zerstört hat. Diese Abfolge führt dazu, dass eine wertlose Variante durch die Pipeline propagiert, was möglicherweise zu kaskadierenden Fehlern führt, wenn nachgelagerter Code davon ausgeht, dass gültige Daten vorhanden sind.
Eine Lösung bestand darin, jeden Zugriff auf die Variante mit valueless_by_exception()-Überprüfungen und manueller Wiederherstellungslogik zu umschließen, bevor irgendwelche Besuchs- oder Abfrageoperationen durchgeführt wurden. Dieser Ansatz bot explizite Sicherheit gegen undefiniertes Verhalten, überfrachtete jedoch den Code mit defensiven Überprüfungen an jedem Nutzungspunkt, verschlechterte die Lesbarkeit erheblich und führte zu unakzeptablen Verzögerungen im kritischen Handelsweg.
Ein anderer Ansatz erwog die Verwendung von std::optional<std::variant<...>>, um den leeren Zustand außerhalb der Variante selbst zu externalisieren. Während dies die interne Invarianz der Variante bewahrte, indem sichergestellt wurde, dass die innere Variante immer einen gültigen Typ hielt, führte es zu einer zweiten Indirektionsschicht und erforderte doppelte Dereferenzierung für jeden Zugriff, was die API-Oberfläche komplizierte und potenziell die Cache-Lokalität bei der Verarbeitung mit hohem Durchsatz beeinträchtigte.
Das Team wählte schließlich std::monostate als erste Alternative in der Variantentypliste aus, wodurch effektiv ein expliziter "leerer" Zustand innerhalb des normalen Typsystems der Variante reserviert wurde. Diese Wahl beseitigte die Möglichkeit des wertlosen Zustands vollständig, da die Variante immer zu std::monostate zurückfallen konnte, anstatt wertlos zu werden, wodurch sichergestellt wurde, dass index() immer eine gültige Position zurückgab und std::visit immer erfolgreich an echte Daten oder den Handler für den leeren Zustand dispatchte.
Das Ergebnis war ein robuster Nachrichtenprozessor, der Zuweisungsfehler elegant handhabte, indem er zum Monostate-Alternativen überging, anstatt in einen Ausnahmefehlerzustand zu gelangen. Dieses Design bewahrte strikte Typensicherheit, ohne zur Laufzeit Überprüfungen auf Wertlosigkeit durchführen zu müssen oder unter dem Overhead doppelter Indirektion zu leiden. Entwickler konnten sich darauf verlassen, dass die Variante immer besuchbar war, wobei der Monostate-Handler als No-Op oder Standardverhalten für leere Nachrichten fungierte.
Warum erlaubt std::variant den Zustand valueless_by_exception, obwohl dies gegen das allgemeine Entwurfsprinzip verstößt, dass eine Variante immer einen ihrer angegebenen Typen halten sollte?
Der Standard priorisiert starke Ausnahmesicherheit über die Aufrechterhaltung der strengen Invarianz um jeden Preis. Bei der Änderung der gehaltenen Alternative muss die Variante den alten Wert zerstören, bevor der neue konstruiert wird, um Ressourcenlecks oder doppelte Eigentumsprobleme zu vermeiden. Wenn diese neue Konstruktion eine Ausnahme auslöst, kann die Variante nicht zum vorherigen Zustand zurückkehren, da dieser Speicher bereits zerstört ist, noch kann sie den Übergang zum neuen Zustand abschließen. Der Zustand valueless_by_exception dient als notwendiger Fluchtweg, um anzuzeigen, dass das Objekt zerstörbar und zuweisbar ist, aber keinen gültigen Alternativen hält, wodurch undefiniertes Verhalten verhindert wird, das daraus resultieren würde, dass man vorgibt, der alte Wert existiere noch oder lasse den Speicher nicht initialisiert.
Wie verhält sich std::visit, wenn es auf eine Variante aufgerufen wird, die in den Zustand valueless_by_exception eingetreten ist, und warum unterscheidet sich dies vom Zugriff auf eine Variante, die std::monostate hält?
std::visit wirft sofort std::bad_variant_access, wenn es auf eine wertlose Variante stößt, da der aktive Typindex variant_npos ist, der nicht auf irgendeine Besucherüberladung abgebildet werden kann. Dies unterscheidet sich grundlegend von std::monostate, die einen legitimen, wenn auch leeren Typ darstellt, der eine spezifische Indexposition innerhalb der Variantentypliste einnimmt. Ein Besucher kann eine spezifische Überladung für std::monostate bereitstellen, um leere Zustände als Teil des normalen Kontrollflusses elegant zu behandeln. Der wertlose Zustand stellt eine wahre Fehlersituation dar, in der die Typinformation vollständig verloren geht, während Monostate einen gültigen, absichtlichen leeren Zustand innerhalb des Typsystems darstellt, der am Mechanismus der Besuchsdispatch teilnehmen kann.
Kann sich eine Variante aus dem Zustand valueless_by_exception erholen, ohne das Variantobjekt selbst zu zerstören und neu zu erstellen, und welche spezifischen Operationen ermöglichen diese Wiederherstellung?
Ja, eine Wiederherstellung ist durch Zuweisungen oder emplace-Operationen möglich, ohne dass das Variante-Wrapper selbst zerstört werden muss. Wenn Sie v = T{} oder v.emplace<T>(args) ausführen und die Konstruktion des Typs T erfolgreich ist, verlässt die Variante den wertlosen Zustand und hält den neuen Typ. Dies funktioniert, weil diese Operationen definiert sind, um eine neue aktive Alternative zu etablieren, die effektiv den Speicher mit einem gültigen Wert erneut initialisiert und den internen Index von variant_npos auf die Position von T zurücksetzt. Das bloße Lesen aus der Variante oder das Aufrufen nicht-modifizierender Beobachter ändert den Zustand nicht; nur eine erfolgreiche Operation, die einen neuen Wert in den Speicher platziert, kann die Klassensinvarianz wiederherstellen und das Wertlosigkeitsflag auf false zurücksetzen.