std::variant werd geïntroduceerd in C++17 als een typeveilige unie-alternatief dat ontworpen is om de foutgevoelige en handmatig beheerde C-stijl unies te vervangen. Het handhaaft de invariant dat het altijd precies één van zijn gespecificeerde alternatieve types bevat, wat compile-time typeveiligheid en intuïtieve waarde-semantiek biedt. Dit ontwerp garandeert theoretisch dat operaties zoals std::visit of std::get altijd een geldig type om op te werken hebben.
De status valueless_by_exception vertegenwoordigt een specifieke foutmodus waarbij de variant geen waarde bevat vanwege een uitzondering die optreedt tijdens type-wijzigende operaties. Deze situatie ontstaat wanneer de variant zijn huidige alternatief moet vernietigen om ruimte te maken voor een nieuw alternatief, maar de daaropvolgende constructie van het nieuwe alternatief een uitzondering gooit. Hierdoor blijft het object zonder een geldige actieve lid, wat tijdelijk de standaardvariant-invariant schendt.
De oplossing die de standaard biedt, is om deze enkele ongeldige status toe te staan om basis-uitzonderingsveiligheidsgaranties te handhaven. Terwijl het zich in deze staat bevindt, blijft de variant vernietigbaar en toewijsbaar, waardoor middelen kunnen worden opgeruimd en nieuwe waarden in de opslag kunnen worden geplaatst. Om volledig van deze toestand te herstellen, moet men met succes een nieuwe waarde toewijzen of plaatsen, wat de invariant herstelt door een geldig alternatief vast te stellen en de interne status bij te werken.
std::variant<std::string, int> v = "hello"; try { v.emplace<std::string>(10000000, 'x'); // kan bad_alloc gooien } catch (...) { assert(v.valueless_by_exception()); v = 42; // Herstel: weer geldig }
Overweeg een high-frequency trading systeem dat marktgegevensberichten verwerkt, voorgesteld als std::variant<PriceUpdate, OrderCancel, TradeExecution>. Tijdens een geheugenbeperkte situatie gooit een poging om een groot TradeExecution object toe te wijzen std::bad_alloc nadat de variant al de vorige PriceUpdate heeft vernietigd om ruimte te maken. Deze reeks resulteert in een waardeloze variant die door de pijplijn heen roept, wat mogelijk veroorzaker voor cascaderende fouten kan zijn als downstream code veronderstelt dat geldige gegevens aanwezig zijn.
Een oplossing hield in dat elke toegang tot de variant werd omwikkeld met valueless_by_exception() controles en handmatige herstel-logica vóór enige bezoek- of ophaaloperaties. Deze benadering bood expliciete veiligheid tegen ongedefinieerd gedrag, maar verrommelde de codebasis met defensieve controles op elk gebruikspunt, wat de leesbaarheid aanzienlijk verminderde en onaanvaardbare latentie in het kritieke handelsproces introduceerde.
Een andere benadering overwoog het gebruik van std::optional<std::variant<...>> om de lege toestand buiten de variant zelf te externaliseren. Hoewel dit de interne invariant van de variant bewaarde door ervoor te zorgen dat de binnenste variant altijd een geldig type bevatte, introduceerde het een tweede laag van afleiding en vereiste dubbele dereferenties voor elke toegang, wat de API-oppervlakte compliceerde en mogelijk de cache-lokaliteit tijdens hoge doorvoerprocessen beïnvloedde.
Het team koos uiteindelijk voor std::monostate als het eerste alternatief in de lijst met varianttypes, waarmee een expliciete "lege" toestand binnen het normale typesysteem van de variant werd gereserveerd. Deze keuze elimineerde de mogelijkheid van de waardeloze toestand volledig omdat de variant altijd kon terugvallen op het bevatten van std::monostate in plaats van waardeloos te worden, wat ervoor zorgde dat index() altijd een geldige positie teruggegeven en std::visit altijd succesvol dispatchte naar ofwel echte gegevens of de lege toestandsbehandelaar.
Het resultaat was een robuuste berichtenverwerker die allocatie-fouten soepel afhandelde door over te schakelen naar het monostate-alternatief in plaats van een uitzonderlijk ongeldige toestand. Dit ontwerp handhaafde strikte typeveiligheid zonder dat runtime-controles voor waardeloosheid nodig waren of leed aan overhead van dubbele afleiding. Ontwikkelaars konden rekenen op de variant altijd bereikbaar te zijn, met de monostate-behandelaar die fungeert als een no-op of standaardgedrag voor lege berichten.
Waarom staat std::variant de staat valueless_by_exception toe ondanks dat deze de algemene ontwerpeisen schendt dat een variant altijd één van zijn gespecificeerde types moet bevatten?
De standaard prioriteert sterke uitzondering veiligheid boven het handhaven van de strikte invariant voor alle kosten. Bij het wijzigen van het gehouden alternatief moet de variant de oude waarde vernietigen voordat de nieuwe wordt geconstrueerd om resource-lekken of dubbele eigendomsproblemen te voorkomen. Als deze nieuwe constructie gooit, kan de variant niet terugschakelen naar de vorige toestand omdat die opslag al is vernietigd, en kan het de overgang naar de nieuwe staat ook niet voltooien. De status valueless_by_exception fungeert als een noodzakelijke ontsnappingsklep die aangeeft dat het object vernietigbaar en toewijsbaar is, maar geen geldig alternatief bevat, wat voorkomt dat ongedefinieerd gedrag optreedt dat zou voortkomen uit het doen alsof de oude waarde nog steeds bestaat of de opslag niet-geïnitieerd laat.
Hoe gedraagt std::visit zich wanneer het wordt aangeroepen op een variant die in de valueless_by_exception status is gekomen, en waarom verschilt dit van het benaderen van een variant die std::monostate bevat?
std::visit gooit onmiddellijk std::bad_variant_access wanneer het een waardeloze variant tegenkomt omdat de actieve type-index variant_npos is, wat niet overeenkomt met een van de bezoeker-overloads. Dit verschilt fundamenteel van std::monostate, dat een legitiem maar leeg type is dat een specifieke indexpositie binnen de type-lijst van de variant bezet. Een bezoeker kan een specifieke overload voor std::monostate bieden om lege toestanden gracieus af te handelen als onderdeel van de normale controle-stroom. De waardeloze staat vertegenwoordigt een echte fouttoestand waar type-informatie volledig verloren is gegaan, terwijl monostate een geldige, opzettelijke lege toestand binnen het typesysteem vertegenwoordigt die deelneemt aan het bezoekdispatchmechanisme.
Kan een variant herstellen uit de valueless_by_exception toestand zonder de variant zelf te vernietigen en opnieuw te construeren, en welke specifieke operaties faciliteren dit herstel?
Ja, herstel is mogelijk via toewijzing of emplace operaties zonder dat het nodig is de variantwrapper zelf te vernietigen. Wanneer je v = T{} of v.emplace<T>(args) uitvoert, en de constructie van type T slaagt, verlaat de variant de waardeloze staat en bevat het het nieuwe type. Dit werkt omdat deze operaties zijn gedefinieerd om een nieuw actief alternatief vast te stellen, waardoor de opslag opnieuw wordt geïnitialiseerd met een geldige waarde en de interne index van variant_npos wordt teruggezet naar de positie van T. Slechts lezen uit de variant of het aanroepen van niet-wijzigende observatoren wijzigt de toestand niet; alleen een succesvolle operatie die een nieuwe waarde in de opslag plaatst kan de klasse-invariant herstellen en de waardeloze vlag op false zetten.