Cf. déjà dit, le proto supportait l' ``héritage conditionnel'' ; depuis cet été j'ai seulement défini la liste des abstractions correspondant aux images (ex : abstract::greylevel_image)
un objectif est de pouvoir définir des morpheurs génériques le plus simplement possible. Le problème est double pour un tel morpheur : - avoir l'héritage qui va bien (résolu par l'utilisation des propriétés) - ne pas avoir à recoder les méthodes que le morpheur doit fournir.
un exemple : les valeurs d'une image (ref) sont vues à travers une fonction (f) => une image morphée (ima). ima[p] renvoit donc f(ref[p]). on doit pouvoir appeler ima.size() pour accéder aux dimensions de l'image vue à travers f mais on n'a pas envie d'écrire cette méthode. si ce type de morpheur est implémenté par la classe fimage<I,F> avec I type de ref et F type de f, on doit redéfinir "op[]" et "op[] const" mais on ne veut pas coder fimage<I,F>::size(). En effet, d'après la description de ce morpheur, la méthode "size" n'est pas transformée (on doit avoir "ima.size()" est équivalent à "ref.size()). Pire, on ne connaît pas la liste des méthodes que l'on devraient écrire ! Si les données de ref sont stockées avec un buffer mémoire linéaire, on peut appeler ref.buffer() pour récupérer son adresse de début et "ref.buffer_size()" pour connaître sa taille (longueur). On doit donc pouvoir faire de même avec ima. En revanche, si ref n'est pas "linéaire", ima ne l'est pas non plus. Comment demander au programmeur qui doit définir la classe paramétrée "fimage" (et ses méthodes) de gérer tous ces cas ?
soluce
dans le proto, on définit des hiérarchies d'abstractions ; par exemple abstract::image dont un bout est donné ci-dessous :
template <typename E> struct image : public internal::get_image_impl < image<E>, E > { unsigned npoints() const // abstract { return this->exact().impl_npoints(); } //... };
ici npoints() est une méthode abstraite et on code la liaison à la main (via this->exact().impl_)
ce qui change par rapport à "avant" (hier et les jours encore avant), c'est le fait qu'on puisse récupérer des implémentations par défaut pour les méthodes. elles arrivent via l'héritage de internal::get_image_impl (notez le "get").
bien sûr, il faut écrire les implémentations par défaut (le "set" qui correspond au "get") :
template <typename E> struct set_image_impl < image<E>, E> : public virtual image_impl<E> { unsigned impl_npoints() const { return this->delegate().npoints(); } //... };
vous voyez qu'ici, ce n'est pas "exact()" (liaison) qui est appelé mais "delegate()" (délégation). On ne rêve pas, une implémentation par défaut signifie qu'on a un objet concret quelque part qui nous fournit cet implémentation. Dans le cas d'un morpheur (ima), c'est bien sûr l'image morphée (ref).
au bout du compte, un morpher "identité" (un morpheur qui ne change rien à l'image ciblée) s'écrit en quelques lignes :
template <typename I> struct id_morpher;
template <typename I> struct props < id_morpher<I> > : public props <I> { typedef I delegated_type; };
template <typename I> struct id_morpher : public abstract::from_image_morpher< I, id_morpher<I> > { typedef abstract::from_image_morpher< I, id_morpher<I> > super; id_morpher(const I& ref) : super(ref) { exact_ptr = this; } };
et pour vérifier que ça marche, on définit ref puis on appelle des méthodes attendues naturellement :
int main() { image2d<int> ref(16, 64); std::cout << ref.npoints() << std::endl;
// le morpheur qui en fout pas une :
id_morpher< image2d<int> > ima(ref);
std::cout << ima.npoints() << std::endl; // comme toute image std::cout << ima(5, 1) << std::endl; // car ref est 2d std::cout << ima.buffer_size() << std::endl; // car ref a un buf.lin. }