Le type std::variant

Ce qui suit est une ébauche d'un article à venir.

Depuis C++ 17, un type std::variant est offert pour représenter une valeur d'un type parmi un ensemble de types. Par exemple, un std::variant<int,std::string,float> pourra contenir un int, ou un std::string, ou encore un float.

#include <variant>
#include <string>
using namespace std;
int main() {
   variant<int, string, float> v = 3; // ici, v contient un int
   v = "Yo"s; // v contient maintenant un std::string
   int n = 3;
   // v = &n; // illégal : v ne peut contenir un int*
}

On peut lire d'un std::variant ce qui y a été le plus récemment entreposé. Ainsi, dans l'exemple qui précède, après avoir affecté "Yo"s à v, il est légal d'en lire une std::string mais pas d'en lire un int. Pour accéder au contenu d'un std::variant, quelques approches possibles sont :

Utiliser la fonction globale std::get(), aussi utilisable (sous une forme semblable) sur des std::tuple ou des std::pair d'ailleurs. Cette fonction permet d'accéder à l'élément par sa position dans la liste des types possibles, de même que par le type lui-même, dans la mesure bien entendu où ce type n'est pas ambigu, ce qui peut se produire – pensez à un std::variant<unsigned int,std::size_t> sur une plateforme où le second est un alias pour le premier.

Avec cette approche, accéder au mauvais élément (p. ex. : faire std::get<int>(v) dans l'exemple à droite) lèvera une exception de type std::bad_variant_access.

#include <variant>
#include <string>
#include <iostream>
using namespace std;
int main() {
   variant<int, string, float> v = 3; // ici, v contient un int
   v = "Yo"s; // v contient maintenant un std::string
   cout << get<1>(v) << endl; // par position
   cout << get<string>(v) << endl; // par type
}

Utiliser la fonction globale std::get_if(). Cette fonction permet d'accéder à l'élément par sa position dans la liste des types possibles, de même que par le type lui-même, dans la mesure bien entendu où ce type n'est pas ambigu, et retourne un pointeur sur cet élément.

Avec cette approche, accéder au mauvais élément (p. ex. : faire std::get_if<int>(v) dans l'exemple à droite) retournera un pointeur nul.

#include <variant>
#include <string>
#include <iostream>
using namespace std;
int main() {
   variant<int, string, float> v = 3; // ici, v contient un int
   v = "Yo"s; // v contient maintenant un std::string
   cout << *get_if<1>(v) << endl; // par position
   cout << *get_if<string>(v) << endl; // par type
}

Si le souhait n'est que de vérifier si un variant contient un élément spécifique, indiqué par son type (qui ne doit pas être ambigu), il est possible d'utiliser le prédicat global std::holds_alternative(). Il n'y a pas de version positionnelle de cette fonction au moment d'écrire ces lignes.

#include <variant>
#include <string>
#include <cassert>
using namespace std;
int main() {
   variant<int, string, float> v = 3; // ici, v contient un int
   v = "Yo"s; // v contient maintenant un std::string
   assert(holds_alternative<std::string>(v)); // par type
}

Cependant, la version la plus chouette de procéder est probablement la fonction std::visit().

La fonction std::visit()

L'une des vocations des std::variant est d'offrir une alternative Type-Safe aux union étiquetés. En effet, il est possible d'agir (ou de réagir) en fonction du type réellement entreposé dans un std::variant par voie d'une application charmante du schéma de conception Visiteur.

Avec un union étiqueté Avec std::visit() sur un std::variant
#include <string>
#include <iostream>
using namespace std;
const int TAILLE_TEXTE = 128;
enum class type_message { entier, texte, reel };
union Contenu {
   int n;
   char s[TAILLE_TEXTE]; // pas une string, car les union, ça fonctionne mieux avec des trucs triviaux
   double d;
};
struct Message {
   type_message lequel;
   Message contenu;
};
Message creer_message() { /* ... */ }
// ...
int main() {
   auto msg = creer_message();
   switch(msg.lequel) {
   case type_message::entier:
      cout << "Entier de valeur " << msg.contenu.n << endl;
      break;
   case type_message::texte:
      cout << "Texte \"" << msg.contenu.s << "\"" << endl;
      break;
   case type_message::reel:
      cout << "Réel de valeur " << msg.contenu.d << endl;
      break;
   default:
      throw "Message corrompu";
   };
}
#include <string>
#include <iostream>
#include <variant>
using namespace std;
using Message = std::variant<int, string, double>;
Message creer_message() { /* ... */ }
// ...
struct Visiteur {
   void operator()(int n) const {
      cout << "Entier de valeur " << n << endl;
   }
   void operator()(const string &s) const {
      cout << "Texte \"" << s << "\"" << endl;
   }
   void operator()(double d) const {
      cout << "Réel de valeur " << d << endl;
   }
};
// ...
int main() {
   auto msg = creer_message();
   visit(Visiteur{}, msg);
}

L'approche par std::visit() est à la fois plus simple et moins risquée, du fait qu'elle impose de couvrir tous les types possibles pour un variant donné et qu'elle ne risque pas de se retrouver en situation d'étiquette incorrecte (le cas default dans l'exemple de gauche).

Si l'action à poser lors d'une visite est générique, alors les λ génériques de C++ 14 nous simplifient singulièrement l'existence :

#include <string>
#include <iostream>
#include <variant>
using namespace std;
using Message = std::variant<int, string, double>;
Message creer_message() { /* ... */ }
// ...
// ...
int main() {
   auto msg = creer_message();
   visit([](auto && val) { cout << val << endl; }, msg);
}

Ici, l'intention se limitant à projeter l'objet visité sur la sortie standard, sans égard à son type, une simple λ générique suffit amplement.

Une combinaison générique

Pour comprendre ce qui suit, mieux vaut se familiariser d'abord avec les templates variadiques.

Si créer une classe pour chaque visite d'un std::variant nous vous convient pas, il est possible de créer un visiteur adéquat à partir d'une agrégation d'expressions λ. Par exemple :

Avec visiteur « manuel » Avec combinaison de λ
 
template <class ... P> struct Combine : P... {
   Combine(P... ps) : P{ ps }... {
   }
   //
   // le using qui suit est nécessaire, mais peut ne pas être
   // supporté sur certains compilateurs en 2017
   //
   using P::operator()...;
};
template <class ... F>
   Combine<F...> combine(F... fs) {
      return { fs ... };
   }
#include <string>
#include <iostream>
#include <variant>
using namespace std;
using Message = std::variant<int, string, double>;
Message creer_message() { /* ... */ }
// ...
struct Visiteur {
   void operator()(int n) const {
      cout << "Entier de valeur " << n << endl;
   }
   void operator()(const string &s) const {
      cout << "Texte \"" << s << "\"" << endl;
   }
   void operator()(double d) const {
      cout << "Réel de valeur " << d << endl;
   }
};
// ...
int main() {
   auto msg = creer_message();
   visit(Visiteur{}, msg);
}
#include <string>
#include <iostream>
#include <variant>
using namespace std;
using Message = std::variant<int, string, double>;
Message creer_message() { /* ... */ }
// ...
int main() {
   auto msg = creer_message();
   visit(combine(
      [](int n) {
         cout << "Entier de valeur " << n << endl;
      },
      [](const string &s) {
         cout << "Texte \"" << s << "\"" << endl;
      },
      [](double d) {
         cout << "Réel de valeur " << d << endl;
      }
   ), msg);
}

Avec Combine et la fonction génératrice combine(), il devient possible de créer des classes de visite au besoin, sans faire d'effort particulier.

Lectures complémentaires

Quelques liens pour enrichir le propos.

Le design de std::variant n'a pas été chose simple, en particulier pour ce qui est du sens à donner à un std::variant vide ou à l'état d'un std::variant si l'affectation d'une valeur y échoue (typiquement, si le constructeur de mouvement de la valeur qui y est affectée lève une exception). À cet effet :


Valid XHTML 1.0 Transitional

CSS Valide !