Maîtriser std::variant : Une alternative type-safe aux unions en C++

Principes fondamentaux

  • Un std::variant<T1, T2, ..., TN> ne peut contenir qu’une seule valeur à la fois — parmi ses types paramétrés — avec suivi automatique de son état.
  • Aucune allocation dynamique n’est effectuée : la mémoire est réservée statiquement selon la taille maximale des alternatives.
  • Les types void, les tableaux, et les références sont interdits ; les types const/volatile peuvent être dupliqués.
  • L’utilisation de std::monostate permet d’introduire un état « vide » explicite, utile notamment pour contourner l’exigence de constructibilité par défaut du premier type.

Initialisation contrôlée

Plusieurs stratégies permettent d’instancier un variant avec précision :

1. Construction par défaut

Si le premier type est constructible par défaut, il est utilisé automatiquement :

std::variant<int, std::string> v{}; // contient int{0}

Pour éviter cette contrainte, placez std::monostate en tête :

std::variant<std::monostate, NonDefaultType, double> safe{};
// safe contient maintenant std::monostate

2. Initialisation directe

Le compilateur sélectionne le type correspondant au littéral fourni — sauf ambiguïté :

std::variant<long, int> v1 = 42L;     // OK : long
std::variant<long, int> v2 = 42;      // Erreur : ambigüité

3. Construction ciblée avec std::in_place_type ou std::in_place_index

Cela permet d’appeler explicitement un constructeur paramétré :

struct Config {
    std::string name;
    int port;
    Config(std::string n, int p) : name(std::move(n)), port(p) {}
};

std::variant<int, Config, bool> cfg{std::in_place_type<Config>, "api", 8080};
// Équivalent à : cfg.emplace<Config>("api", 8080);

Accès sécurisé aux valeurs

La récupération d’une valeur doit toujorus tenir compte de l’état réel du variant :

Approche robuste avec std::get_if

Retourne un pointeur valide ou nullptr, sans exception :

std::variant<int, std::string, double> data = "test";

if (auto* s = std::get_if<std::string>(&data)) {
    *s += " (modifié)";
} else if (auto* i = std::get_if<int>(&data)) {
    *i *= 2;
}

Visite polymorphique avec std::visit

Permet d’apliquer un comportement spécifique selon le type actuel, sans code répétitif :

template<typename... Ts>
struct Visitor : Ts... { using Ts::operator()...; };
template<typename... Ts>
Visitor(Ts...) -> Visitor<Ts...>;

std::variant<int, std::string, double> val = 3.14;

std::visit(Visitor{
    [](int x) { std::cout << "Entier: " << x << "\n"; },
    [](const std::string& s) { std::cout << "Chaîne: " << s << "\n"; },
    [](double d) { std::cout << "Flottant: " << d << "\n"; }
}, val);

Gestion avancée

  • v.index() renvoie l’indice (0-based) du type actif, ou std::variant_npos si l’état est valueless_by_exception.
  • v.valueless_by_exception() indique un échec partiel lors d’une opération (ex. : exception levée pendant un emplace).
  • std::holds_alternative<T>(v) teste la présence d’un type sans déclencher d’exception.
  • v.emplace<T>(args...) remplace la valeur courante par une nouvelle instance construite in-place.

Quand privilégier std::variant ?

  • Lorsque le jeu de types possibles est connu statiquement (par opposition à std::any).
  • Pour modéliser des structures discriminées comme des AST, des protocoles réseau ou des formats sérialisés.
  • Quand la performance est critique : pas d’allocation dynamique ni d’effacement de type.
  • En combinaison avec std::visit, pour implémenter des dispatchs de type sûrs et extensibles.

Étiquettes: std-variant cpp17 union-alternative type-safety

Publié le 21 août à 06h57