• Aucun résultat trouvé

Notre solution à base de déduction de type et de classe imbriquée

Notre solution suit le travail de McNamara and Smaragdakis. En effet, nous gardons l’idée de leur solution pour améliorer la signature des fonctions en C++. Rappelons que notre but est de proposer une approche où le code à écrire est simple, à la fois pour un utilisateur et un programmeur. Remarquons qu’un utilisateur connaît toujours le type de l’image qu’il souhaite traiter. A partir de point d’entrée, la déduction des types de sorties et des types intermédiaires est automatique.

L’écriture et l’utilisation d’algorithmes sont à peu près du même ordre de difficulté que FC++. Notre solution repose également sur l’utilisation de classe imbriquée. Contrairement à la méthode de McNamara et Smaragdakis, une classe imbriquée définit à la fois le "trait" pour le type de retour et l’opérateur parenthèse. Cette classe imbriquée est paramétrée par le type d’entrée de l’algorithme. Une méthode similaire à celle de FC++ est utilisée pour effectuer la déduction de type.

Contrairement à ce qui est généralement fait, nous définissons un algorithme en deux étapes (d’où les classes imbriquées). La première étape gère une fonctionnalité "abstraite" d’un algorithme sans prendre en compte le type des données en entrées. Par exemple, twice est la notion abstraite ; c’est le fait de doubler une quantité et ce, peu importe le type de cette quantité. La deuxième étape sert à spécialiser un algorithme "abstrait" pour un type d’entrée particulier, par exemple, la fonction prend en entrée des entiers. Évidemment, seuls les algorithmes spécialisés sont capables d’effectuer les calculs. Les avantages d’une telle construction sont les suivants : d’une part, ces deux types d’algorithmes sont des types. Cela signifie qu’un utilisateur ou un programmeur peut les référencer à la compilation en utilisant

le mot clef typedef du C++. D’autre part, une fois qu’un algorithme est défini de manière abstraite - c’est-à-dire sans avoir précisé le type des entrées - et qu’il a été spécialisé pour une entrée particulière, l’obtention du type de retour est immédiate : il est donné par le membre Output. Remarquons que cette approche se situe entre le style de programmation de STL et celui de FC++.

Nous détaillons maintenant notre solution pour la composition. Nous montrons égale- ment les différences entre l’approche proposée par FC++ et la nôtre.

D.6.1 Le cas d’une fonction simple

Nous commençons avec l’exemple de la fonction twice. Rappelons-nous la figureD.1où un algorithme est présenté comme une fonction qui prend des entrées et déduit son type de retour à partir du type de ces entrées une fois que celles-ci sont connues. Notre solution reflète ce diagramme en définissant le membre Output à l’intérieur d’une classe imbriquée qui est paramétrée par le type de son entrée. Nous obtenons le code suivant :

struct twice {

template <class Input> struct toReal {

typedef ... Output;

Output operator()(const Input & x) { return 2*x; }

}; };

Comme nous pouvons le voir, la seule différence entre notre solution et celle de FC++ est que la classe imbriquée définit à la fois le type de retour et l’opérateur parenthèse, operator(). Nous appelons cette classe imbriquée toReal puisqu’elle fait référence à l’action de spécialiser un algorithme "abstrait". Contrairement à FC++, cette solution impose d’écrire explicitement le type d’entrée de l’algorithme avant de pouvoir l’utiliser. En revanche, le type de retour est facilement obtenu par le membre Output. Voici un exemple d’utilisation :

typedef twice::toReal<int> algo; std::cout << algo()(3) << std::endl; algo::Output i = algo()(3);

std::cout << i << std::endl;

Comme nous pouvons le voir, l’appel à une fonction simple comme twice n’est pas aussi pratique que dans la bibliothèque FC++. En revanche, la classe d’algorithmes est désormais désignée par un type. Nous montrons maintenant l’avantage que cela apporte.

D.6.2 Composition et algorithmes avec variations

Nous présentons maintenant notre méthode pour la composition. Nous rappelons la figure D.3 qui présente le type de composition verticale, ce qui correspond au cas d’un algorithme présentant une variation. Remarquons que ce diagramme est récursif : en effet, la variation a le même diagramme que l’algorithme lui-même.

F. D.4 – Diagramme pour une composition en utilisant notre solution. Le type de retour est décrit par un triangle. Il est déduit et stocké par chaque sous algorithme. Les lignes fléchées représentent la propagation des types.

Le diagramme pour la composition de chaînage d’algorithmes devrait également pré- senter ce motif récursif ; mais le diagramme de la figureD.2, qui présente cette composition, n’est pas récursif. Nous présentons en figure D.4un autre diagramme pour la composition avec la propagation des types. Comme nous pouvons le voir, la seule différence entre une composition horizontale (concaténation) et verticale (variation) est que deux sous-blocs sont présents dans la version horizontale alors qu’un seul est nécessaire pour la version verticale. Autrement dit, il n’y a pas de différence intrinsèque entre les deux versions.

Contrairement à une fonction simple, une fonction d’ordre supérieur est directement paramétrée par les fonctions internes qui la compose. Pour la composition des fonctions F et Gon a donc le code suivant pour la première étape :

template<class F, class G> struct compose {...};

Le résultat de la composition de deux fonctions unaires est donc un type. Nous écrivons compose<twice,twice>pour la composition de twice par twice. Ce type est maintenant utilisable comme une fonction simple (en fait, c’est même une fonction simple). La structure doit définir le type réel des fonction F et G et doit également définir le type de retour. Enfin, l’opérateur parenthèse operator() définit le calcul à effectuer. Le code correspondant à la deuxième étape est le suivant :

template<class Input> struct toReal

{

typedef typename G::toReal<Input> _G; typedef typename F::toReal<typename _G::Output> _F;

typedef typename _F::Output Output;

Output operator()(const Input& x) { return _F()(_G()(x)); }

};

Contrairement à FC++, nous n’avons pas besoin de découper le code de la composition en deux parties. Ceci est possible car par abus de langage on peut dire que les algorithmes sont

des types. On notera que les instances des algorithmes ne sont créées que lorsque les calculs sont effectués.

Puisque tous les algorithmes sont des types, un utilisateur peut utiliser le mot clef typedef du C++ pour manipuler les algorithmes sans prendre en compte le type des entrées. Par exemple, nous pouvons écrire :

typedef compose<twice, twice> Algo1_; typedef compose<twice, twice> Algo2_; typedef compose<Algo2, Algo1_> FinalAlgo_;

Une fois qu’un algorithme est écrit, l’utilisateur précise pour quels types en entrée, l’algo- rithme doit s’instancier. Pour instancier l’algorithme de l’exemple précédant, FinalAlgo_, avec une entrée de type entier, nous écrivons alors :

typedef int Input;

typedef FinalAlgo_::toReal<Input> FinalAlgo; FinalAlgo::Output i = FinalAlgo()(3);

Peu importe la définition de l’algorithme (i.e, le nombre de combinaisons qu’il a fallu faire pour le définit), le type de retour est toujours donné par le "trait" Output.

Nous avons donc réussi à définir un mode de composition qui permet de créer des fonctions d’ordre supérieur. Avec notre méthode, le type de retour des algorithmes est facile à obtenir. En revanche, les algorithmes doivent être instanciés manuellement pour le type d’entrée que l’on souhaite utiliser.

Enfin remarquons que nous n’utilisons pas le mot clef virtuel du langage C++. Par conséquent la résolution des appels de fonctions et méthodes est réalisée à la compilation. L’optimiseur du compilateur est donc capable d’enlever ces appels de fonctions (inliner en anglais), et donc de produire un code efficace (contrairement à une résolution dynamique des appels).

D.6.3 Objets pratiques dédiés au traitement des images

Contrairement à FC++, notre solution permet de définir des objets qui sont capables de stockerdes variables. Par exemple, supposons que l’on désire calculer le minimum d’une séquence d’éléments. L’objet fonction, utilisant notre approche, qui met en œuvre cet algo- rithme est donné ci-dessous :

struct findMin { template <Input> struct toReal {

toReal(): count(0)

void reset() { count = 0;}

void operator()(const Input & x) {

if (count == 0) { min = x;} else {

if (x < min) min = x; }

}

int count; Input min; };

};

Nous rappelons que ce code est écrit une fois pour toute et est réutilisable.