L’arrivée du C++17 – std::variant

Bonjour à tous ! L’article d’aujourd’hui clôture le trio entamé par std::any et std::optional, avec std::variant, introduit dans le standard depuis C++17.

Introduction

std::optional<T> est encore une fois basée sur son homologue boost:optional<T>. Ce type vise à permettre la création d’union type-safe.

Une union, pour rappel, est une construction héritée du C permettant de créer des types a états :

Les unions ont deux gros problèmes. Premièrement, elles ne sont pas type-safe, dans le sens où il n’est pas possible de tester quel est le type contenu ou d’avoir une erreur en cas de mauvaise manipulation.

De plus, les unions ne sont pas faite pour travailler avec des types complexes et ne gèrent pas la destruction des objets. Je vous propose ci-dessous l’exemple de cppreference qui illustre bien la problématique :

Ici, il est nécessaire d’effectuer des appels explicites aux destructeurs et des placement new de manière à faire fonctionner correctement l’union.

C’est ici qu’intervient std::variant, qui est une union type-safe qui gère elle même la durée de vie des objets lors du changement du type contenu. Voici le même exemple avec  std::variant :

std::variant en détail

Création d’un std::variant

Comme pour std::any<T> et std::optional<T>, std::variant<T> propose un certain nombre de méthodes de création :

Notez qu’ici, les deductions guides ne permettent pas d’inférer le type du variant lors de la construction.

std::variant propose aussi std::monostate, qui est le type choisi lorsque aucun autre type n’est initialisé, notamment lorsque les types ne sont pas constructibles par défaut :

Deux std::monostate sont toujours égaux.

Récupérer la valeur contenue

Pour récupérer la valeur d’un std::variant, il y a trois choix : std::get, std::get_if et std::visit (qui sera abordé un peu plus tard puisque son cas est particulier).

std::get permet d’accéder à la valeur voulue soit en spécifiant le type souhaité, soit l’index de ce type (dans les paramètres template du std::variant) :

Si le type demandé n’est pas le type actuellement contenu, une exception std::bad_variant_access est levée.

std::get_if retourne un pointeur sur l’objet demandé. Si le type demandé n’est pas le bon, nullptr est retourné :

Modifier la valeur

L’affectation est possible sur std::variant :

std::get et std::get_if permettent de modifier la valeur, l’un retournant une référence, le second une constante :

La fonction membre emplace permet de remplacer la valeur courante in-place : 

Visiteur

Nous arrivons maintenant à la fameuse fonction std::visit.

std::visit prends un visiteur en paramètre. Le visiteur doit posséder une surcharge pour chaque type possible du std::variant. Par conséquent, les foncteurs, les lambdas ou encore std::function peuvent être passés en paramètre ( visit attends un Callable) :

Une fonction membre générique dans un foncteur ou une lambda générique permettent de traiter plusieurs cas en même temps.

La fonction std::visit  peut prendre plusieurs std::variant en paramètre. Dans ce cas, le visiteur doit être capable de gérer tous les types de toutes les  std::variant. Depuis C++20, il existe également une surcharge de  std::visit permettant de préciser le type de retour (qui prime alors sur le type déduit).

Il y a également une proposition en cours pour std::overload, qui permettrait de facilité l’écriture des foncteurs :

Cette fonction pourra être simplement utilisée avec std::variant :

Notez que dans ce cas, chaque lambda peut avoir ses propres captures ce qui n’est pas possible avec un foncteur.

Cet article propose une implémentation en attendant la validation de la proposition, et explique d’ailleurs comment s’en servir pour simuler du pattern matching :

Autres

Pour conclure cette section, je vous propose de passer en revue les autres opérations possibles sur std::variant.

La fonction membre index retourne la position du type actuellement contenu :

La fonction   std::holds_alternative retourne un booléen déterminant si un type particulier est actuellement actif :

La fonction membre valueless_by_exception retourne un booléen permettant de savoir si le std::variant est actuellement dans un état invalide. En effet, si une exception a lieu lors de l’affectation par copie ou par mouvement, le  variant est garanti d’être dans un état invalide. L’état du variant peut optionnellement être invalide si une exception survient pendant une affectation ou un appel à emplace changeant le type contenu.

Si un  std::variant est dans un état invalide (c’est-à-dire si valueless_by_exception retourne true), index retourne std::variant_npos  et std::get et std::visit lèvent std::bad_variant_access.

La comparaison entre des std::variant permet de comparer le type contenu entre deux variant ayant la même liste de paramètres templates. Cette page sur cppreference.com résume les règles de comparaison, qui prennent en compte si le variant est dans un état invalide, si le type contenu est le même et qui le cas échéant effectuent la comparaison sur l’objet contenu (si les objets ne peuvent être comparé, il s’agit d’un undefined behavior).

Les traits std::variant_size et  std::variant_alternative (et les helpers correspondant), premettent respectivement de récupérer le nombre de types possibles et le type d’un index spécifique.

std::variant dispose aussi d’une spécialisation pour std::hash et std::swap.

Cas d’usage

En terme d’empreinte mémoire, std::variant occupe au minimum la taille du type le plus large possible. On remarque que de l’espace additionnel est nécessaire pour stocker les informations sur le type actuel et que la taille varie en fonction de l’alignement :

Comme une union classique, aucun espace additionnel n’est alloué par std::variant lors des différentes opérations.

D’une manière général, std::variant est un choix judicieux dès lors que vous savez que vous aller manipuler un ensemble fini de types. Si l’ensemble est potentiellement infini, dirigez-vous plutôt vers std::any.

std::variant est donc à considérer comme une union type-safe, qui gère la durée de vie des objets en appelant les destructeurs au bon moment.

Je vous remercie pour votre lecture et je vous dis à la prochaine fois =).

Références et ressources

%d bloggers like this: