Pages

dimanche 20 octobre 2013

Faire un flux qui redirige vers d'autres flux (std::ostream).

Je prend l'exemple type: la création d'un logger. Il est courant de voir ce genre de classe dans un projet souvent utilisé à travers une macro pour y insérer date, fichier et numéro de ligne. L'idée est bonne mais l'implémentation de la classe logger est souvent trop compliquée.

Cette classe manipule généralement les flux std::cerr ou une instance de std::ofstream. Voire même dans certaines implémentations une multitude de flux. En gros, elle fait la passerelle entre des données et le(s) flux où écrire.

En partant de ce constat, on peut imaginer voir un header un peu comme ça:

class Logger {
public:
  switch_stdout(bool);
  switch_stderror(bool);
  //...

  Logger & operator << (int);
  Logger & operator << (const char *);
  //etc

private:
  std::ofstream log {"/tmp/log"};
  std::ofstream trace {"/tmp/trace"};
  bool to_cerr = false;
  bool to_cout = false;
};

Un problème est que cette classe n'est pas du tout compatible avec les flux. La raison en est toute simple, ce n'en est pas un !
Du fait de cette non compatibilité, une utilisation courante comme `Logger() << type_perso()` ne fonctionnera pas s'il n'y a pas la surcharge de type_perso pour Logger ET pour std::ostream

Ce point est facile à corriger en remplaçant toutes les surcharges de << par une template. En plus ça réduit le nombre de lignes :)
Cependant, le Logger n'étant pas un flux, d'autres problèmes peuvent exister.

Le plus gros problème reste l'apparente complexité pour écrire dans les flux. Tout ce mal alors que std::ostream fait déjà presque tout. Et que dire de std::clog qui rien que par son nom semble être fait pour ça ?

Les flux ont en interne un buffer, un std::streambuf pour être plus précis. Sa fonction est de prendre des chaînes de caractères et de les écrire quelque part. Ou inversement, de lire une chaîne.
Difficile de faire plus bas niveau.

Bien évidemment, les buffers d'un flux son interchangeables, il m'est tout à fait possible de rediriger std::cout dans un fichier en quelques lignes de code seulement.

int main()
{
  std::filebuf sbuf;
  if (!sbuf.open("/tmp/out")) {
    //error
    return 1;
  }
  std::cout.rdbuf(&sbuf);
  std::cout << "j'écris dans un fichier\n";
}

Au final, Logger fait la même chose mais sans la gestion d'erreur et sans flux de sortie entièrement personnalisable.

Dans mon idée, logger peut simplement contenir le buffer de log (std::clog.rdbuf() par défaut) et fournir au moins 1 méthode pour fournir un buffer avec la responsabilité de le détruire. Le reste est gérer automatique par l'héritage sur std::ostream. ou via une méthode pour fournir le flux.

void Logger::give_buffer_ownership(std::unique_ptr<std::streambuf> buf)
{
  delete internal_buffer;
  internal_buffer = buf.release();
  internal_ostream.rdbuf(internal_buffer);
}

Éventuellement un moyen de créer un objet intermédiaire qui ajoute la date, nom de fichier, etc dans le constructeur et flush automatiquement à sa destruction (vive le RAII).