std::variant a été introduit dans C++17 comme une alternative de type sûr aux unions, conçue pour remplacer les unions en C de style C, sujettes aux erreurs et gérées manuellement. Il impose l'invariant selon lequel il détient toujours exactement un de ses types alternatifs spécifiés, fournissant une sécurité de type à la compilation et une sémantique de valeur intuitive. Ce design garantit théoriquement que des opérations telles que std::visit ou std::get ont toujours un type valide sur lequel opérer.
L'état valueless_by_exception représente un mode de défaillance spécifique où le variant ne contient aucune valeur en raison d'une exception survenue lors d'opérations modifiant le type. Cette situation survient lorsque le variant doit détruire son alternative actuelle pour faire place à une nouvelle, mais la construction subséquente de la nouvelle alternative entraîne une exception. Par conséquent, l'objet est laissé sans membre actif valide, ce qui brise temporairement l'invariant standard du variant.
La solution fournie par la norme est de permettre cet état invalide singulier spécifiquement pour maintenir les garanties de sécurité de base concernant les exceptions. Pendant cet état, le variant reste destructible et assignable, permettant le nettoyage des ressources et l'attribution de nouvelles valeurs dans le stockage. Pour se rétablir complètement de cette condition, il faut assigner ou emplacer avec succès une nouvelle valeur, ce qui restaure l'invariant en établissant une alternative valide et en réinitialisant le suivi de l'état interne.
std::variant<std::string, int> v = "hello"; try { v.emplace<std::string>(10000000, 'x'); // peut provoquer un bad_alloc } catch (...) { assert(v.valueless_by_exception()); v = 42; // Récupération : valide à nouveau }
Considérez un système de trading à haute fréquence traitant des messages de données de marché représentés comme std::variant<PriceUpdate, OrderCancel, TradeExecution>. Lors d'un scénario à mémoire contrainte, une tentative d'assignation d'un grand objet TradeExecution provoque std::bad_alloc après que le variant ait déjà détruit le précédent PriceUpdate pour faire de la place. Cette séquence entraîne un variant sans valeur se propageant à travers le pipeline, pouvant causer des échecs en cascade si le code en aval suppose que des données valides sont présentes.
Une solution impliquait d'encapsuler chaque accès au variant avec des vérifications valueless_by_exception() et une logique de récupération manuelle avant toute opération de visite ou de récupération. Cette approche offrait une sécurité explicite contre le comportement indéfini mais encombrait le code avec des vérifications défensives à chaque point d'utilisation, dégradant considérablement la lisibilité et introduisant une latence inacceptable dans le chemin critique du trading.
Une autre approche envisagée consistait à utiliser std::optional<std::variant<...>> pour externaliser l'état vide en dehors du variant lui-même. Bien que cela préservât l'invariant interne du variant en garantissant que le variant interne détenait toujours un type valide, il introduisait une seconde couche d'indirection et nécessitait un double déréférencement pour chaque accès, compliquant la surface de l'API et pouvant impacter la localité du cache lors du traitement à haut débit.
L'équipe a finalement sélectionné std::monostate comme première alternative dans la liste des types du variant, réservant effectivement un état "vide" explicite au sein du système de types normal du variant. Ce choix a éliminé la possibilité de l'état sans valeur car le variant pouvait toujours revenir à la détention de std::monostate au lieu de devenir sans valeur, garantissant que index() renvoie toujours une position valide et que std::visit soit toujours dispatché avec succès vers soit des données réelles, soit le gestionnaire d'état vide.
Le résultat fut un processeur de messages robuste qui gérait gracieusement les échecs d'allocation en transitionnant vers l'alternative monostate plutôt qu'un état invalide exceptionnel. Ce design maintenait une stricte sécurité de type sans nécessiter de vérifications d'exécution pour la non-valeur ou subir un surcoût d'indirection double. Les développeurs pouvaient compter sur le fait que le variant était toujours visitable, le gestionnaire monostate agissant comme un comportement par défaut ou une opération nulle pour les messages vides.
Pourquoi std::variant autorise-t-il l'état valueless_by_exception malgré la violation du principe général de conception selon lequel un variant doit toujours détenir un de ses types spécifiés ?
La norme privilégie une forte sécurité d'exception plutôt que de maintenir l'invariant strict à tout prix. Lorsqu'il change l'alternative détenue, le variant doit détruire l'ancienne valeur avant de construire la nouvelle pour prévenir les fuites de ressources ou les problèmes de double possession. Si cette nouvelle construction propage une exception, le variant ne peut pas revenir à l'état précédent car ce stockage est déjà détruit, ni ne peut compléter la transition vers le nouvel état. L'état valueless_by_exception sert de sortie nécessaire indiquant que l'objet est destructible et assignable mais détient aucune alternative valide, évitant ainsi des comportements indéfinis qui résulteraient de la prétention que l'ancienne valeur existe encore ou de laisser le stockage non initialisé.
Comment std::visit se comporte-t-il lorsqu'il est invoqué sur un variant qui est entré dans l'état valueless_by_exception, et pourquoi cela diffère-t-il de l'accès à un variant tenant std::monostate ?
std::visit lance immédiatement std::bad_variant_access lorsqu'il rencontre un variant sans valeur car l'indice de type actif est variant_npos, ce qui n'est associé à aucune surcharge de visiteur. Cela diffère fondamentalement de std::monostate, qui est un type légitime bien que vide occupant une position d'indice spécifique au sein de la liste des types du variant. Un visiteur peut fournir une surcharge spécifique pour std::monostate afin de gérer les états vides avec élégance dans le cadre du flux de contrôle normal. L'état sans valeur représente une véritable condition d'erreur où l'information de type est totalement perdue, tandis que monostate représente un état vide valide et intentionnel au sein du système de types qui participe au mécanisme de dispatch de visite.
Un variant peut-il se rétablir de l'état valueless_by_exception sans détruire et reconstruire l'objet variant lui-même, et quelles opérations spécifiques facilitent cette récupération ?
Oui, la récupération est possible par des opérations d'assignation ou emplace sans avoir besoin de détruire l'enveloppe du variant elle-même. Lorsque vous exécutez v = T{} ou v.emplace<T>(args), et que la construction du type T réussit, le variant sort de l'état sans valeur et détient le nouveau type. Cela fonctionne parce que ces opérations sont définies pour établir une nouvelle alternative active, réinitialisant effectivement le stockage avec une valeur valide et réinitialisant l'indice interne de variant_npos à la position de T. Seules des opérations réussies qui placent une nouvelle valeur dans le stockage peuvent restaurer l'invariant de classe et réinitialiser le drapeau de non-valeur à faux.