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 typesconst/volatilepeuvent être dupliqués. - L’utilisation de
std::monostatepermet 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, oustd::variant_npossi l’état estvalueless_by_exception.v.valueless_by_exception()indique un échec partiel lors d’une opération (ex. : exception levée pendant unemplace).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.