Consommer du texte ligne par ligne

Ce qui suit se veut un petit truc simple pour consommer du texte ligne par ligne avec un effort minimal. À titre d'exemple, ce qui suit afficherait chaque ligne du fichier in.txt sur la sortie standard :

// ... inclusions et using ...
int main() {
   ifstream in{ "in.txt" };
   for (const auto & s : lignes{ in }) {
      cout << s << endl;
   }
}

L'idée derrière cette manoeuvre :

La classe lignes n'est pas un conteneur, mais elle exposera des services nommés begin() et end() pour qu'une instance de lignes s'intègre bien dans une répétitive for sur des intervalles.

Notez que j'aurais aussi pu spécialiser std::begin() et std::end() sur lignes, une option que je n'ai pas eu le temps d'explorer.

Sur le plan structurel, un lignes contient à la fois une référence sur le flux d'où sont consommées les lignes, et la ligne la plus récemment lue.

#include <fstream>
#include <string>
class lignes {
   std::istream &is;
   std::string cur;
   void read_one() {
      std::getline(is, cur);
   }

Lors de la construction d'une instance de lignes, je consomme une première ligne. Ceci peut placer l'objet en question dans un état d'erreur, équivalent à un objet modélisant un fichier vide.

public:
   lignes(istream &is) : is{ is } {
      read_one();
   }

L'essentiel du travail est réalisé par la classe lignes::iterator. Cette classe modélise le concept de ForwardIterator, et ne permet qu'une lecture du début à la fin, ce qui permet de consommer des lignes de texte au clavier ou sur un lien réseau.

   class iterator {
   public:
      using value_type = string;
      using iterator_category = forward_iterator_tag;
      using difference_type = int;
      using pointer = value_type*;
      using const_pointer = const value_type*;
      using reference = value_type&;
      using const_reference = const value_type&;

Pour faire son travail, un lignes::iterator contient un pointeur sur le lignes qui lui sert de source de données (et qui peut être nul). Le constructeur prenant en paramètre un lignes* est privé, mais lignes est amie de lignes::iterator donc tout se tient.

   private:
      lignes *src {};
      friend class lignes;
      iterator(lignes *src) noexcept : src{ src } {
      }
   public:
      iterator() = default;

Puisque lignes ne modélise que des lignes de texte, lignes::iterator n'a pas besoin d'exposer l'opérateur -> et peut se limiter à offrir l'opérateur *.

      reference operator*() noexcept {
         return src->cur;
      }
      const_reference operator*() const noexcept {
         return src->cur;
      }

L'opérateur == est plus complexe qu'on pourrait s'y attendre, pour tenir compte du fait que typiquement, la comparaison entre deux instances de lignes::iterator impliquera au moins une instance qui contiendra un src qui sera nul.

      bool operator==(const iterator &other) const noexcept {
         return (src == other.src) || (!src && !other.src->is) || (!src->is && !other.src);
      }
      bool operator!=(const iterator &other) const noexcept {
         return !(*this == other);
      }

Enfin, les opérateurs ++, préfixe et suffixe, consomment une ligne de la source. Le service lignes::read_one() est privé dans lignes, mais heureusement lignes::iterator est ami de lignes.

      iterator& operator++() {
         src->read_one();
         return *this;
      }
      iterator operator++(int) {
         auto temp = *this;
         operator++();
         return temp;
      }
   };

Enfin, les services clés de lignes sont begin() et end(), qui retournent des lignes::iterator convenablement construits.

   iterator begin() noexcept {
      return{ this };
   }
   iterator end() noexcept {
      return{ };
   }
private:
   friend class iterator;
};

Voilà!


Valid XHTML 1.0 Transitional

CSS Valide !