Visiteurs génériques

Ce qui suit est né d'un échange en 2015 entre Jens Weller (un très chic type) et moi-même. L'ami Jens souhaitait implémenter un visiteur générique mais n'avait pas accès à un compilateur C++ 14 (avec lequel la solution est triviale), alors nous avons échangé quelques tentatives d'en arriver à un palliatif acceptable.

Je pense que ce bref article, en montrant les contorsions intellectuelles requises pour en arriver à nos fins avec des compilateurs plus anciens, donnera une idée de l'avancée conceptuelle que représentent les λ génériques de C++ 14.

Le problème sur la table était (je paraphrase) :

« Comment implémenter un visiteur générique, au sens où :

D'entrée de jeu, mieux vous en informer tout de suite : sans λ génériques, les solutions sont tout sauf simples et légères. Nos échanges ont eu lieu en partie sur Twitter par messagerie privée, alors les gens qui n'ont vu que le début et la fin ont constaté la solution idéale, mais dans ce cas, nous la connaissions d'office et nous cherchions à faire « pas si mal » sans elle.

Essai 00

Un premier jet pour explorer le domaine du problème et cerner ce qu'on veut faire ou pas.

#include <iostream>
#include <string>
using namespace std;
struct Caller {
   template <class T>
      auto operator()(const T &obj) -> decltype(&T::name) {
         return &T::name;
      }
};
struct X {
   string name() const { return "X"; }
};
int main() {
   X x;
   auto meth_ptr = Caller{}(x);
   cout << (x.*meth_ptr)() <<<< endl;
}

Cette proposition avait trait au problème tel qu'énoncé initialement (que je n'ai pas répété ici), au sens où on souhaitait un mécanisme de visite qui retournerait (pour les fins de l'exemple) l'adresse d'une méthode d'instance qui pourrait être appelée par la suite.

Le principal défaut de cette approche est que le nom de la méthode doit être encodé à même l'objet utilisé pour la visite (ici : le Caller). À ma connaissance, si nous voulons escamoter (en apparence) cette contrainte d'encodage statique du nom du service à appeler, il faudra avoir recours à une macro qui générera un Callable_xxx étant donné xxx le nom de la méthode souhaitée.

Essai 01

Une autre approche est d'encoder le type de retour du service à appeler dans le type du visiteur statique, d'encoder le nom du service à appeler dans la méthode générique de visite, et de rendre le visiter responsable non pas de retourner l'adresse de la méthode à appeler mais bien d'appeler cette méthode tout simplement.

#include <iostream>
#include <string>
using namespace std;
template <class R>
   struct static_visitor {
      template <class T>
         R operator()(const T &obj) const {
            return obj.name();
         }
   };
struct Person {
   string name() const { return "Bob"; }
};
struct Critter {
   string name() const { return "Grr"; }
};
int main() {
   static_visitor<string> v;
   cout << v(Person{}) << '\n' << v(Critter{}) << endl;
}

Ceci fonctionne, mais il faut un visiteur statique par sorte de méthode à appeler, ce qui demande de réécrire beaucoup de code à chaque fois.

Essai 02

Pour découpler du visiteur l'extraction d'une méthode, on passe ensuite à un foncteur générique qui pourra être associé, couplé au visiteur et réaliser l'appel de la méthode pour lui.

#include <iostream>
#include <string>
using namespace std;
template <class E>
   struct static_visitor_ {
      E extractor;
      static_visitor_(E extractor) : extractor{extractor} {
      }
      template <class T>
         auto operator()(const T &obj) const -> decltype(extractor(obj)) {
            return extractor(obj);
         }
      template <class T>
         auto operator()(T &obj) const -> decltype(extractor(obj)) {
            return extractor(obj);
         }
   };
template <class E>
   static_visitor_<E> static_visitor(E extractor) {
      return static_visitor_<E>{extractor};
   }
struct Person {
   string name() const { return "Bob"; }
};
struct Critter {
   string name() const { return "Grr"; }
};
struct get_name_extractor {
   template <class T>
      string operator()(const T &arg) const {
         return arg.name();
      }
};
int main() {
   auto v = static_visitor(get_name_extractor{});
   cout << v(Person{}) << '\n' << v(Critter{}) << endl;
}

Ici, on a ce qu'on voulait, soit un visiteur générique auquel on associe le service d'extraction de méthode qui nous convient (et qui pourrait être un service de manipulation de l'objet visité au sens large), construit avec une fonction génératrice pour alléger un peu la syntaxe, mais le recours au foncteur générique nous force à faire une ségrégation entre le point d'appel et le code qu'on lui associe. On écrit beaucoup pour arriver à notre résultat.

Essai 03

Pour poursuivre le découplage du visiteur et de l'extraction d'une méthode, on passe ensuite à un un métafoncteur, qui pourra lui aussi être associé, couplé au visiteur et réaliser l'appel de la méthode pour lui. Cette approche est complexe mais très flexible.

#include <iostream>
#include <string>
using namespace std;
template<template <class> class MF>
   struct static_visitor {
      template<class T>
         auto operator()(const T &obj) -> decltype((obj.*MF<T>{}())()) {
            return (obj.*MF<T>{}())();
         }
   };
struct Person {
   string name() const { return "Bob"; }
};
struct Critter {
   string name() const { return "Grr"; }
};
template <template <class> class MF, class T>
   auto apply_visit(static_visitor<MF> v, T obj) -> decltype(v(obj)) {
      return v(obj);
   }
template <class T>
   struct name_ {
      auto operator()() const -> decltype(&T::name) {
         return &T::name;
      }
   };
int main() {
   static_visitor<name_> v;
   cout << apply_visit(v, Person{}) << '\n';
   cout << apply_visit(v, Critter{}) << endl;
}

Ici encore, on a ce qu'on voulait :

Notez qu'on aurait pu aller plus loin et dissocier le métafoncteur du visiteur pour le rapporter à la méthode, pour qu'un même visiteur statique puisse être couplé à plusieurs métafoncteurs d'extraction pendant sa vie.

Solution C++ 14

Ce qui suit est l'approche (plus charmante!) pour résoudre le problème en C++, mais requiert un compilateur C++ 14. C'est Jens lui-même qui l'a publié, et qui aurait sans doute préféré n'écrire que cela.

#include <iostream>
#include <string>
using namespace std;
struct Person {
   string name() const { return "Bob"; }
};
struct Critter {
   string name() const { return "Grr"; }
};
template<class F>
   struct generic_member_visitor {
      template <class T>
         string operator()(F f, const T &obj) const {
            return f(obj);
         }
   };
int main() {
   auto lambda = [](const auto& obj){ return obj.name(); };
   generic_member_visitor<decltype(lambda)> gmc;  
   cout << gmc(lambda, Person{}) << endl;
   cout << gmc(lambda, Critter{}) << endl;
}

C'est, vous en conviendrez, beaucoup plus simple et beaucoup plus léger. Comme dans le cas précédent, le visiteur aurait peu être découplé du type de l'opération à appliquer, ce dernier rapporté à la méthode de visite en tant que telle, pour élargir le champ d'application du visiteur :

#include <iostream>
#include <string>
using namespace std;
struct Person {
   string name() const { return "Bob"; }
};
struct Critter {
   string name() const { return "Grr"; }
};
struct generic_member_visitor {
   template <class F, class T>
      auto operator()(F f, const T &obj) const  {
         return f(obj);
      }
};
int main() {
   auto lambda = [](const auto& obj) { return obj.name(); };
   generic_member_visitor gmc;
   cout << gmc(lambda, Person{}) << endl;
   cout << gmc(lambda, Critter{}) << endl;
}

Ceci, je pense, met en relief quelques-uns des progrès que nous a apporté C++ 14.

Mise à jour de dernière heure : Jens Weller a publié cette charmante bribe de code :

template <class R, class lambda>
   struct visitor : lambda, public boost::static_visitor<R> {
      visitor(const lambda &c)
         :lambda(c)
      {
      }
   };

Approche alternative (forme abrégée), qui permet l'héritage multiple :

[]:boost::static_visitor<std::string> (const auto &f){...};

C'est beau, la programmation, parfois... Et Boost est plein de perles!

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !