mercredi 15 juillet 2009

EDIT 17/07/2009: Suite à un commentaire de mon bon ami Andy, j'ai apporté quelques modifications au code.

Tous ceux qui se sont amusés un tant soit peu avec les flux d'entrée en C++ ont probablement déjà entendu parler des std::istream_iterator. Ils permettent de parcourir un flux entrée (une instance d'une classe héritant de std::istream) comme si c'était une séquence. Un appel à l'opérateur ++ d'un tel flux revient à utiliser l'opérateur >> sur le flux, et on peut ensuite utiliser les opérateurs * et -> pour accéder à l'élément précédemment lu. Un std::istream_iterator est unidirectionnel (on ne peut qu'avancer dans le flux) et en lecture seule (évidemment, puisque l'on s'en sert pour lire d'un flux). Un std::istream_iterator constant n'a donc pas de sens.

L'idéal est de passer par un exemple. Imaginons que nous voulons lire, dans un fichier, un ensemble de nombres entiers pour ensuite les insérer dans un vecteur. La première solution serait d'avoir recours à une boucle dans laquelle on insère les entiers lus dans le vecteur avec push_back(). Ou bien, on peut également faire la même chose en une ligne en mélangeant std::copy, un std::istream_iterator et un std::back_inserter:


std::ifstream file("hello.txt");
std::vector<int> v;
std::copy(std::istream_iterator<int>(file), std::istream_iterator<int>(), std::back_inserter(v));
std::cout << v.size();

Une petite explication s'impose. istream_iterator possède deux constructeurs: l'un deux reçois un flux d'entrée (std::istream) à parcourir. Le second (celui qui ne prend pas de paramètres) représente un flux invalide. Ainsi, dans notre exemple précédent, si le flux tombe dans un état invalide (la fin du fichier, par exemple), alors les deux itérateurs deviendront égaux, permettant de sortir de la boucle.
On copie donc du début (premier paramètre) à la fin (second paramètre) du flux, en envoyant les éléments à un std::back_inserter, lequel ne fait que passer l'élément reçu à la méthode push_back() du vecteur.

Malheureusement, std::istream_iterator utilise >> pour lire les données. Mais que se passe-t-il si l'on veut lire avec la si utile fonction std::getline()?

D'où l'idée que j'ai eu de coder un getline_iterator. Il fonctionne exactement comme un std::istream_iterator, mais utilise std::getline() plutôt que l'opérateur >> pour lire les données.

On commence donc par inclure les fichiers nécessaires: iterator, string et istream.
La classe est une classe template, spécialisée d'après un certain type de caractères. std::getline() ne lit en effet que des chaînes de caractère, il est donc inutile de la rendre plus générique. On peut également spécifier des traits de caractère, et les traits de base du type de caractère spécifié sont utilisés par défaut.

La classe hérite de std::iterator, et spécifie trois choses: basic_getline_iterator est un itérateur d'entrée (input_iterator), il lit des éléments de type std::basic_string<CharType, Traits>, et la distance entre les éléments est de type std::basic_string<CharType, Traits>::size_type (la même que pour une string, donc).

#ifndef GETLINE_ITERATOR_H
#define GETLINE_ITERATOR_H

#include <iterator>
#include <string>
#include <istream>

template<class CharType, class Traits = std::char_traits<CharType> >
class basic_getline_iterator : public std::iterator<std::input_iterator_tag,
std::basic_string<CharType, Traits>,
typename std::basic_string<CharType, Traits>::size_type>
{

Pour garantir l'intéropérabilité avec les algorithmes et classes de la STL, on définit quelques typedefs. Seuls les cinq premiers sont obligatoires. Le dernier, stream_type, est là pour simplifier la syntaxe, plus loin. Je l'ai laissé en public car je ne voyais pas vraiment la nécessité de le cacher.

public:
typedef std::input_iterator_tag iterator_category;
typedef typename std::basic_string<CharType, Traits> value_type;
typedef typename std::basic_string<CharType, Traits>::size_type distance;
typedef value_type & reference;
typedef value_type * pointer;

typedef typename std::basic_istream<CharType, Traits> stream_type;

On a trois attributs: l'élément en lu lors du dernier appel à l'opérateur ++ (un basic_string), un pointeur sur le flux (nous verrons pourquoi un pointeur dans quelques instants) et le caractère faisant office de séparateur (utile pour std::getline()).

private:
value_type _current;
stream_type * _stream;
const CharType _separator;

Le premier constructeur (celui qui représente un flux invalide) ne fait qu'assigner 0 au pointeur, et \0 au séparateur (le séparateur doit être initialisé puisqu'il est constant). Le second constructeur reçoit une référence sur un std::istream (tous les flux sont incopiables, donc une référence est obligatoire), et assigne l'adresse de ce flux au pointeur. Un paramètre optionnel peut également être passé pour faire office de séparateur. Par défaut, il s'agit du retour de ligne (ça ne s'appelle pas "getline" pour rien).

public:
basic_getline_iterator() : _stream(0), _separator('\0') {}
basic_getline_iterator(stream_type & stream, char separator = '\n') : _stream(&stream), _separator(separator) {}

L'opérateur++ est disponible en version préfixée (première version) et postfixée (seconde version). La version préfixée appelle std::getline en passant les paramètres requis, puis teste la valeur de retour de la fonction. std::getline() retourne ne effet le flux passé comme premier paramètre. Ce flux peut être convertit implicitement en void *. Si le pointeur est nul, alors le flux est invalide. Donc, s'il est invalide, on assigne 0 au pointeur. Suite à un commentaire, j'ai décidé d'ajouter un test à l'opérateur ++ pour empêcher un plantage lamentable si quelqu'un venait à incrémenter le pointeur alors que l'itérateur est invalide.
La version postfixée créée une copie, appelle la version préfixée, puis retourne la copie.

basic_getline_iterator operator++() throw()
{
if(_stream)
{
if(!std::getline(*_stream, _current, _separator))
_stream = 0;
}
return *this;
}
basic_getline_iterator operator++(int) throw()
{
basic_getline_iterator it(*this);
operator++();
return it;
}

Une simple comparaison de pointeurs. Ainsi, si les deux itérateurs représentent des flux invalides, ils seront considérés comme égaux.

bool operator==(const basic_getline_iterator &it) const throw()
{
return _stream == it._stream;
}
bool operator!=(const basic_getline_iterator &it) const throw()
{
return !(*this == it);
}

Déréférencer l'itérateur ne fait que retourner une référence (ou un pointeur) sur l'élément précédemment lu.

const value_type & operator *() const throw()
{
return _current;
}
const value_type * operator->() const throw()
{
return &_current;
}

Pour finir, on offre deux typedefs correspondants aux deux utilisations principales de basic_getline_iterator. Voilà!

};

typedef basic_getline_iterator<char> getline_iterator;
typedef basic_getline_iterator<wchar_t> wgetline_iterator;
#endif


Rien de mieux qu'un exemple:

#include<iostream>
#include <fstream>
#include <string>
#include <iterator>
#include <algorithm>
#include <vector>
#include "getline_iterator.h"

int main()
{
// lire les lignes d'un fichier puis les insérer dans un vecteur
std::ifstream file("test.txt");
std::vector<std::string> v;
std::copy(getline_iterator(file), getline_iterator(), std::back_inserter(v));
}


Cet itérateur est encore en développement, et il se peut que je revienne apporter des modifications (notamment la const-correctness). À suivre.

2 commentaires:

Andy a dit…

1. bool operator==(const basic_getline_iterator &it) throw()
2. {
3. return _stream == it._stream;
4. }
5. bool operator!=(const basic_getline_iterator &it) throw()
6. {
7. return !(*this == it);
8. }


Const correctness ici entre autre s'il vous plait.

et si je me fis a la signature de std::getline fournis par www.cplusplus.com/reference

istream& getline ( istream& is, string& str, char delim );


# basic_getline_iterator operator++() throw()
# {
# if(!std::getline(*_stream, _current, _separator))
# _stream = 0;
# return *this;
# }


si _stream = 0 au moment de l'appel de operator++()

alors *0... et pire

&(*0)



sinon bravo, très intéressant.

Etienne de Martel a dit…

Le risque de déréférencement de pointeur nul est by-design. Je considère que le risque d'appeler l'op ++ sur un itérateur invalide est très faible, et ça ne vaut pas la peine d'ajouter un test pour compenser une mauvaise utilisation.

Je respecte la philosophie du C++: le programmeur sait ce qu'il fait.

Enregistrer un commentaire