Visiter plusieurs std::variant

Ce qui suit est une ébauche d'un article à venir. Mieux vaut avoir lu un peu sur std::variant au préalable.

Merci à Björn Fahller pour l'inspiration (bien involontaire) dans ce qui suit.

Supposons que votre programme doive opérer sur deux (ou plus) std::variant à la fois, par exemple dans le cas où vous utilisez ce type pour implémenter un petit langage incluant des calculs arithmétiques (où les opérandes peuvent être entières, réelles, booléennes, etc.).

De prime abord, utiliser std::visit() et combine() (voir variant.html#combine pour des détails) semble raisonnable. Toutefois, le souci de découvrir les deux types à visiter implique deux appels à std::visit(), ce qui rappelle la complexité des multiméthodes. À titre d'exemple (très simplifié) :

#include <variant>
#include <iostream>
using namespace std;
template <class ... P> struct Combine : P... {
   Combine(P... ps) : P{ ps }... {
   }
   // ce using 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 ... };
   }

auto combine_(variant<string, int> v0, variant<string, int> v1) {
   return visit(combine(
      [&](int n) {
         return visit(combine(
            [&](int m) { cout << n << ';' << m << endl; },
            [&](const string &s) { cout << n << ';' << '\"' << s << '\"' << endl; }
         ), v1);
      },
      [&](const string &s0) {
         return visit(combine(
            [&](const string &s1) { cout << '\"' << s0 << '\"' << ';' << '\"' << s1 << '\"' << endl; },
            [&](int n) { cout << '\"' << s0 << '\"' << ';' << n << endl; }
         ), v1);
      }
   ), v0);
}

int main() {
   combine_(3, "Yo"s);
}

Remarquez l'effort investi dans combine_(), soit une visite initiale pour chaque type envisageable à titre de première opérande, puis une visite additionnelle pour chaque type envisageable à titre de seconde opérande. Ceci peut être allégé pas des λ génériques lorsque des cas généraux s'appliquent à plusieurs types, mais dans le cas où le cas par cas, justement, survient, il se trouve que la visite de deux opérandes fait croître rapidement la complexité du problème.

Cette complexité rappelle celle des multiméthodes, où la découverte successive des opérandes de gauche et de droite fait croître rapidement la complexité du problème rencontré.

Il se trouve que cette complexité peut être aplanie en logeant en une seule et même fonction (ici, multicombine()) la logique de la double visitation, et en faisant en sorte que cette fonction rappelle une fonction à deux opérandes dont les types auront été découverts de prime abord. Par exemple :

#include <variant>
#include <iostream>
using namespace std;
template <class ... P> struct Combine : P... {
   Combine(P... ps) : P{ ps }... {
   }
   // ce using 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 ... };
   }

template <class C>
   auto multicombine(variant<string, int> v0, variant<string, int> v1, C comb) {
      return visit([&](auto && a) { return visit([&](auto && b) {
         return comb(std::forward<decltype(a)>(a), std::forward<decltype(b)>(b));
      }, v1); }, v0);
   }

int main() {
   multicombine(3, "Yo"s, combine(
      [](int, const string&) { cout << "int string" << endl; },
      [](int, int) { cout << "int int" << endl; },
      [](const string&, int) { cout << "string int" << endl; },
      [](const string&, const string&) { cout << "string string" << endl; }
   ));
}

Ici, le programme de test supplée des fonctions binaires (à deux opérandes) pour les combinaisons de types possibles, et ne doit plus se charger de la complexité inhérente à une double visite.

Cela dit... Björn Fahller m'a fait remarquer que ce mécanisme était déjà implémenté pour un nombre arbitraire de std::variant à même std::visit(). Ainsi, il est possible d'écrire simplement :

#include <variant>
#include <iostream>
using namespace std;
template <class ... P> struct Combine : P... {
   Combine(P... ps) : P{ ps }... {
   }
   // ce using 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 ... };
   }

/*
 ... d'une manière plus générale :
 
 template <class C, class ... Ts>
   auto multi_combine(std::variant<Ts...> v0, std::variant<Ts...> v1, C comb) {
      return std::visit(comb, v0, v1);
   }

*/

template <class C>
   auto multicombine(variant<string, int> v0, variant<string, int> v1, C comb) {
      return visit(comb, v0, v1);
   }

int main() {
   visit(3, "Yo"s, combine(
      [](int, const string&) { cout << "int string" << endl; },
      [](int, int) { cout << "int int" << endl; },
      [](const string&, int) { cout << "string int" << endl; },
      [](const string&, const string&) { cout << "string string" << endl; }
   ));
}

Quel outil sympathique, vraiment.

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !