jeudi 23 janvier 2014

typage, template et sauce tomate en C++…

Mais encore ? En répondant à une question sur StackOverflow je me suis dit qu'un petit article valait le coup sur le sujet.

La question était (voir SO pour plus de détails) en gros :

J'aimerais simplifier l'écriture de cout << a << ',' << b ',' << c << endl; est-il possible d'utiliser un truc du genre myPrinter(cout) << a << b << c << endl; ? On m'a dit que oui, comment faire ?
Bon c'est sûr que c'est un truc de fainéant mais intéressons-nous à la question car il y a quelques petites choses à dire.

L'idée est assez simple finalement : utiliser un type (myPrinter) qui implémente des opérateurs de sortie sur flux et ajoute à chaque écriture une virgule. So let's go :
#include <iostream>
using namespace std;
class myPrinter {
  ostream &o;
public:
  myPrinter(ostream &o) : o(o) {}
  template <typename T> myPrinter &operator<<(const T &t) {
    o << t << ',';
    return *this;
  }
};
int main() {
  myPrinter(cout) << 25 << 100.5 << "hello";
}
On encapsule le flux dans une instance de la classe, puis on redéfinit tous les sorties via un template de méthode surchargeant l'opérateur <<.
Deux problèmes apparaissent :

  1. (pas grave) la dernière impression est toujours suivie par une virgule,
  2. (grave) si on essaie d'envoyer endl le code ne compile pas (essayez-vous même), et le message est vraiment pénible à décoder (punition standard avec les templates foireux).
Occupons-nous du cas grave. En fait, on se méprend généralement sur la nature de endl. Il y a fort à parier que presque n'importe qui répondrait qu'il s'agit d'une constante dont la valeur est la fin de ligne sur le OS local. Évidemment (?!) non. Why? Parce que sinon cela imposerait une sémantique de gestion de la fin de ligne au flux lui-même. C'est donc une fonction, laquelle est appelée en retour par le flux lorsqu'on l'envoie sur le flux; la fonction écrivant alors sur le flux les caractères nécessaires pour réaliser une fin de ligne puis en général déclenche le flush()! endl est donc typé comme une fonction prenant un flux en entrée et renvoyant un flux. Il nous faut donc rajouter une méthode permettant de gérer endl (et autres bizarreries du même genre) :

#include <iostream>
using namespace std;
class myPrinter {
  ostream &o;
public:
  myPrinter(ostream &o) : o(o) {}
  template <typename T> myPrinter &operator<<(const T &t) {
    o << t << ',';
    return *this;
  }
  myPrinter &operator<<(ostream& (*pf)(ostream&)) {
    o << pf;
    return *this;
 }
};
int main() {
  myPrinter(cout) << 25 << 100.5 << endl << 123 << endl;
}
Yeah!

Maintenant le cas simple. Pour le régler n'importe qui se jetterait sur une solution utilisant un booléen permettant de déterminer si on est au début d'un affichage ou après un endl et donc éviterait d'afficher la virgule de trop, comme ici :
#include <iostream>
using namespace std;
class myPrinter {
  ostream &o;
  bool start;
public:
  myPrinter(ostream &o) : o(o), start(true) {}
  template <typename T> myPrinter &operator<<(const T &t) {
    if (start) o << t;
    else o << ',' << t;
    start = false;
    return *this;
  }
  myPrinter &operator<<(ostream& (*pf)(ostream&)) {
    o << pf;
    start = true;
    return *this;
 }
};
int main() {
  myPrinter(cout) << 25 << 100.5 << "hello" << endl << 123 << endl;
}
Yeah!

L'inconvénient (à mes yeux) est qu'on fait un test à chaque sortie (test du booléen). Est-ce vraiment utile ? Non bien sûr! Il suffit de se rappeler que le mécanisme que l'on cherche est un simple automate a deux états (sortie avec virgule, sortie sans virgule) et qu'on bascule de l'un à l'autre à l'impression d'un endl. Alors on va implémenter cet automate via des types! Ce qui donne
#include <iostream>
using namespace std;
class myPrinter {
private:
  class myPrinter2{
    myPrinter &s;
  public:
    myPrinter2(myPrinter &s) : s(s) {}
    template <typename T> myPrinter2 &operator<<(const T &t) {
      s.o << ',' << t ;
      return *this;
    }
    myPrinter &operator<<(ostream& (*pf)(ostream&)) {
      s.o << pf;
      return s;
    }
  };
  ostream &o;
  myPrinter2 s2;
public:
  myPrinter(ostream &o) : o(o), s2(*this) {}
  template <typename T> myPrinter2 &operator<<(const T &t) {
    o << t;
    return s2;
  }
  myPrinter &operator<<(ostream& (*pf)(ostream&)) {
    o << pf;
    return *this;
  }
};
int main() {
  myPrinter(cout) << 25 << 100.5 << endl << 123 << endl;
}
Yeah! Et le tour est joué. Qu'avons-nous fait ? Nous avons laissé le compilateur dérouler un automate sur les types successifs d'une expression...  Finalement rien de mystérieux.

Attention ce code contient peut-être un bug. En effet, il n'y a peut-être pas que endl qui soit une fonction.  Sauriez-vous réparer la classe en conséquence ? Je n'ai pas de réponse simple et immédiate sous la main...

Sinon, sauriez-vous implanter un automate plus compliqué ? I will show you some day...

DjiBee

Aucun commentaire:

Enregistrer un commentaire