1 commentaires mercredi 22 juillet 2009

Java était (et est toujours, d'ailleurs) un langage "purement orienté objet". Quelle décision étrange. Non seulement Java est, selon moi, moins orienté objet que C++ (presque pas de contrôle sur la fin de vie d'un objet, notamment), mais ça voulait également dire qu'il ne pouvait faire rien d'autre que de la POO. Pourtant, je me dis que du procédural, parfois, c'est utile.

C# répète à peu près les mêmes erreurs que Java pour la POO, mais compense en s'ouvrant à d'autres paradigmes. Dans sa version 3.0, C# a donc mis un pied (ou plutôt le gros orteil) dans le monde de la programmation fonctionnelle avec des lambda expressions et des expression trees (bon, on pouvait déjà faire ça depuis 1998 en C++ avec des templates, mais bel effort quand même). Et depuis sa toute première version, C# supporte un hybride entre méthodes et fonctions libres sous la forme de délégués (un pointeur déguisé sur une fonction, en fait). Bon, d'accord, je pourrais également faire le parallèle avec C++, mais pourquoi taper sur quelqu'un qui est à terre?

Je sais pas pour vous, mais moi, écrire une classe pour ne l'utiliser qu'une seule fois, comme foncteur sans état en plus, je trouve ça lourd. C'est d'ailleurs pourquoi, en C++, je préfère les fonctions aux foncteurs lorsque c'est possible: c'est beaucoup moins verbeux (mais bon, une fonction n'a pas d'état, alors il faut parfois sortir les foncteurs). Alors bon, quand je vois, dans le .NET Framework, une méthode qui attend un IEqualityComparer<T>, je sacre. Pourquoi devrais-je implémenter une classe pour une fonctionnalité qui n'existera que là? Ne serait-il pas plus simple de passer un délégué à la place? Martin Fowler n'a-t-il pas dit que la généricité préventive était néfaste?

J'ai donc retroussé mes manches, et voici ce à quoi je suis arrivé:


public static class EqualityComparer
{
private class DelegateComparer<T> : IEqualityComparer<T>
{
private readonly Func<T, T, bool> _equalityFunction; // prend deux T en paramètre et retourne un bool
private readonly Func<T, int> _hashCodeFunction; // prend un T en paramètre et retourne un int

public DelegateComparer(Func<T, T, bool> equalityFunction, Func<T, int> hashCodeFunction)
{
if (equalityFunction == null || hashCodeFunction == null)
throw new ArgumentNullException();
_equalityFunction = equalityFunction;
_hashCodeFunction = hashCodeFunction;
}

public bool Equals(T x, T y)
{
return _equalityFunction(x, y);
}

public int GetHashCode(T obj)
{
return _hashCodeFunction(obj);
}
}

public static IEqualityComparer<T> Make<T>(Func<T, T, bool> EqualityFunction, Func<T, int> HashCodeFunction)
{
return new DelegateComparer<T>(EqualityFunction, HashCodeFunction);
}
}

Bon, quel intérêt? Eh bien, admettons (cet exemple utilise des lambdas, mais vous pourriez très bien passer des méthodes, tant qu'elles ont la bonne signature):

var absComparer = EqualityComparer.Make<int>(
(p1, p2) => Math.Abs(p1) == Math.Abs(p2),
p => Math.Abs(p).GetHashCode());

Et voilà, un IEqualityComparer<T> en une ligne (étalée sur plusieurs lignes pour plus de clarté)! C'est tu pas wonderful?

Dites-moi ce que vous en pensez. Comme toujours, ce n'est pas final.

2 commentaires 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.