OVH Cloud OVH Cloud

Definition d'une interface (a la Java) a l'aide de templates

62 réponses
Avatar
Aurélien REGAT-BARREL
Bonjour à tous,
Je voudrais traduire une interface au sens pur, vraiment logique (ensemble
de méthodes). Je ne veux pas passer par l'héritage et les classes abstraites
de bases, car notamment je ne veux pas de polymorphisme. Je me suis donc
rabatu sur les templates.
Le but est de traduire un shéma UML où divers objets implémentent une
interface ITriplet qui stipule que :
- l'objet est construit à partir de 3 valeurs A, B et C (=> un triplet)
- l'objet possède une méthode GetValue(int) permettant d'accéder à ces 3
valeurs
- l'objet possède une méthode statique GetTripletName() qui retourne le nom
du triplet

Le truc étant que la plupart des objets font des modifications sur A, B et C
avant de les stocker. Les divers objets qui implémentent ITriplet n'ont rien
en commun et ne doivent pas être comparables, pas mélangeables, ... d'où pas
de polymorphisme.

Voilà les solutions auxquelles j'ai pensé, quel est votre opinion :

Pour l'exemple, un seul objet ABC inmplémente ITriplet.

1- on laisse le programmeur faire tout ce qu'il faut (on le guide au
maximum)

#define INTERFACE(x) /**/
#define IMPLEMENTS(x) /**/
#define WHERE(x) /**/

INTERFACE( ITriplet
{
public:
ITriplet( double A, double B, double C );
double GetValue( int Index ) const;
static std::string GetTripletName();
};
)

// triplet ABC
class ABC IMPLEMENTS( ITriplet )
{
public:
ABC( double A, double B, double C )
{
data.reserve( 3 );
data.push_back( A );
data.push_back( B );
data.push_back( C );
}
double GetValue( int Index ) const
{
return this->data.at( Index );
}
static std::string GetTripletName()
{
return "A.B.C";
}
private:
std::vector<double> data;
};

template <typename T>
WHERE( T : ITriplet )
T TripletBuilder()
{
return T( 0, 1, 2 );
}

int main()
{
ABC abc = TripletBuilder<ABC>();
}

2- on a bien une interface ITriplet sous forme de template. C'est plus
l'aspect macro qui est exploité ici

// functor pour construire le triplet
typedef std::vector<double> (TripletCtor)( double A, double B, double C );
// functor pour récupérer son nom
typedef std::string (TripletGetName)();

// interface ITriplet
template<TripletCtor Func, TripletGetName Name>
class ITriplet
{
public:
ITriplet( double A, double B, double C ) :
data( Func( A, B, C ) )
{
}
double GetValue( int Index ) const
{
return this->data.at( Index );
}
static std::string GetTripletName()
{
return Name();
}
private:
std::vector<double> data;
};

// triplet ABC
std::vector<double> ComputeABC( double A, double B, double C )
{
std::vector<double> result;
result.reserve( 3 );
result.push_back( A );
result.push_back( B );
result.push_back( C );
return result;
}
std::string GetNameABC()
{
return "A.B.C";
}

typedef ITriplet<ComputeABC, GetNameABC> ABC;

3 - fournir un template de vérification des méthodes attendues. Au lieu de
donner une interface au programmeur comme en 1, on donne un test à faire
passer à son objet.

template<typename T>
class ITripletImplementationChecker
{
public:
ITripletImplementationChecker()
{
double a = 0.0;
double b = 0.0;
double c = 0.0;
T t( a, b, c );
t.GetValue( 0 );
t.GetValue( 1 );
t.GetValue( 2 );
T::GetTripletName();
}
};

ITripletImplementationChecker<ABC> check_abc;


Comme j'ai l'impression de faire n'importe quoi, j'aimerais avoir votre
avis.
Merci de votre aide.

--
Aurélien REGAT-BARREL

10 réponses

Avatar
Franck Branjonneau
"Aurélien REGAT-BARREL" écrivait:

Du coup je pige pas pourquoi ton exemple fonctionne


Parce que dans

class ABC: public ITriplet<ABC> ...

quand tu écris "ITriplet<ABC>", la classe ABC a déjà été déclarée (au
début de la ligne).


Oui, mais quand tu écris ITriplet<ABC>, le template ITriplet va être
instancié avec ABC, or ABC n'est pas encore implémenté...
C'est comme faire (pour ce que j'ai compris) :

class A;
class B : public A
{
};
class A
{
};



Plutôt

class B;

class A {

// Utilisation de B type incomplet

};

class B: A {};

sauf que là ça marche... pour moi les templates ça marche un peu comme les
macros ( ~ génération de code),


Tu compare une expression règulière à une grammaire (attribuée).

mais apparement c'est pas le cas. J'ai toujours pas compris :-/
--

Franck Branjonneau



Avatar
Franck Branjonneau
Loïc Joly écrivait:

Fabien LE LEZ wrote:

On Mon, 12 Jul 2004 15:44:36 +0200, "Aurélien REGAT-BARREL"
:

Oui, mais quand tu écris ITriplet<ABC>, le template ITriplet va être
instancié avec ABC, or ABC n'est pas encore implémenté...
En fait, je ne sais pas du tout si c'est légal, mais ça a l'air de

marcher. Y a-t-il quelqu'un dans la salle qui sache comment
fonctionnent les templates ?


Je sais pas trop comment, mais en tout cas, je sais que ça marche. Ca
porte même un nom : CRTP ( Curiously Recurring Template
Pattern). Regarde par exemple :
http://www.informit.com/articles/article.asp?p1473&seqNum=3

(c'est extrait du livre de Jossutis et Vandevoorde sur les templates)


(Je suis fatigué -- vérifier les conneries que je raconte)

Quand on écrit

template< typename >
struct U {

};

struct V: U< V > {

};

Il y a /instanciation implicite/ de U avec le paramètre V car hériter
d'une classe nécessite que cette classe soit définie.
Le /point d'instanciation/ se situe avant "l'enclosing namespace"
du machin qui à déclencher l'instanciation.
--
Franck Branjonneau



Avatar
Franck Branjonneau
drkm écrivait:

template < typename T >
class TripletDefaultStorage
{
protected:
TripletDefaultStorage(
T first , T second , T third
)
{
myDatas[ 0 ] = first ;
myDatas[ 1 ] = second ;
myDatas[ 2 ] = third ;
}
T getValue( int index ) const {
return myDatas[ index ] ;
}
private:
T myDatas[ 3 ] ;
} ;


Pourquoi un getter et pas un simple triplet à la std::pair<> ?
D'autant que tous les membres sont protégés.
Si le getter se justifie pourquoi pas une fonction avec un argument
template ? Quelque chose comme

enum Ordinal { first, second, third };

template< Ordinal _ordinal >
getValue() { return myDatas[_ordinal];

Ce qui évite

class TripletDoCheck
class TripletNoCheck


tout en assurant une vérification at compile time.
--
Franck Branjonneau

Avatar
drkm
Franck Branjonneau writes:

drkm écrivait:

Franck Branjonneau writes:

[ Des conneries. ]



Je précise que je n'ai pas du tout écrit cela :-O

--drkm


Avatar
Isammoc
Bonjour à tous,
Je voudrais traduire une interface au sens pur, vraiment logique (ensemble
de méthodes). Je ne veux pas passer par l'héritage et les classes
abstraites

de bases, car notamment je ne veux pas de polymorphisme. Je me suis donc
rabatu sur les templates.


Pourtant c'est la classe abstraite qui est le mode défini pour faire une
interface en C++ si je ne m'abuse.
C'est + restrictif que le java, menfin...

Avatar
Franck Branjonneau
drkm écrivait:

Franck Branjonneau writes:

drkm écrivait:

Franck Branjonneau writes:

[ Des conneries. ]



Je précise que je n'ai pas du tout écrit cela :-O


C'est moi qui dit : j'ai écrit des conneries.
--
Franck Branjonneau



Avatar
drkm
Franck Branjonneau writes:

drkm écrivait:

Pourquoi un getter et pas un simple triplet à la std::pair<> ?


Parce que ça faisait partie de la spécification du PO, il me semble.
Et que le but de la manoeuvre est justement d'encapsuler le stockage
des valeurs. Si elles ne sont pas stockées conjointement dans toutes
les implémentation de le politique, comme c'est le cas dans mon
exemple, cela complique les choses.

D'autant que tous les membres sont protégés.
Si le getter se justifie pourquoi pas une fonction avec un argument
template ? Quelque chose comme

enum Ordinal { first, second, third };

template< Ordinal _ordinal >
getValue() { return myDatas[_ordinal];


Bof. Qu'apporte le template ? Mais l'énumération n'est pas une
mauvaise idée, si l'on sait que l'on ne manipulera que des triplets.

element_type getValue( Ordinal i ) {
return myDatas[ i ] ;
}

Bien que, ... Une énumération qui définit first = 0, second = 1 et
third = 2 n'est pas très utile. Il est aussi clair d'utiliser les
entiers directement.

Ce qui évite

class TripletDoCheck
class TripletNoCheck


tout en assurant une vérification at compile time.


Et si les indices ne sont pas disponibles à la compilation ? Dans
ce cas, on laisse le choix à l'utilisateur. Soit il se charge des
vérifications, à chaque fois, ce qui peut être contraignant mais est
peut-être déjà réalisé à un niveau supérieur.

Soit il délègue cette vérification à notre classe ; il doit juste le
préciser à l'instantiation. Mais surtout, cela sert à lui permettre
de spécifier le comportement à adopter en cas d'erreur : lancer une
exception (quelle exception), arrêter le programme, donner une valeur
par défaut (il suffit de créer une implémentation de la politique dont
check() prend une référence non-constante), etc. Et cela en moins de
10 lignes.

Et s'il veut un comportement de politique modifiable à l'exécution,
se basant sur l'héritage :

class MyCheckBase
{
public:
virtual void doCheck( int i ) = 0 ;
} ;

class MyCheckImpl1
{
public:
virtual void doCheck( int i ) {
// ...
}
} ;

// ...

class MyCheck
{
public:
MyCheckBase * myImpl ;
MyCheck( MyCheckBase * c )
: myImpl( c )
{
}
void check( int i ) {
myImpl->doCheck( i ) ;
}
} ;

On a pour cela besoin d'une interface étendue, au moins le champ en
publique, comme ici. Mais comme le modèle hérite en publique de ses
arguments, l'utilisateur peut étendre l'interface selon ses besoins.

--drkm


Avatar
drkm
Franck Branjonneau writes:

drkm écrivait:

Franck Branjonneau writes:

drkm écrivait:

Franck Branjonneau writes:

[ Des conneries. ]



Je précise que je n'ai pas du tout écrit cela :-O


C'est moi qui dit : j'ai écrit des conneries.


Je sais. Mais ce n'était pas clair.

--drkm




Avatar
drkm
Fabien LE LEZ writes:

On Mon, 12 Jul 2004 19:36:36 +0200, drkm :

Pourrais-tu préciser un
peu, stp ?


Je crois bien qu'il a dit s'être trompé.


Yep. Je n'avais pas reçu cet article lorsque j'ai écrit le mien.
Désolé.

--drkm


Avatar
Aurélien REGAT-BARREL
Pourtant c'est la classe abstraite qui est le mode défini pour faire une
interface en C++ si je ne m'abuse.
C'est + restrictif que le java, menfin...


Oui mais étant abstraite je peux pas créer un vecteur de triplets par
exemples. Ou alors il faut passer par les pointeurs. Le problème de la
gestion mémoire & du polymorphisme se posent alors. Je peux du coup mélanger
des triplets dans un même tableau, ou comparer 2 triplets de types
différents, ce qui n'a pas de sens.
Je ne dois même pas avoir la possibilité de traiter un triplet au sens
général :
Affiche( const ITriplet & ); // ne doit pas être possible
Pourquoi ? Parce que dans mon cas ça n'a pas de sens. On affiche un ABC, un
DEF ou un GHI, mais pas un Triplet. Il est important que le programmeur
explicite toujours le type de triplet manipulé. ITriplet n'est là que par
commodité, c'est un détail d'implémentation. A un niveau d'abstraction plus
élevé, il n'existe que ABC, DEF et GHI qui n'ont rien en commun, sinon le
fait d'avoir 3 valeurs.
Le problème : il faut écrire autant de fonctions qu'il y a de triplets :
Affiche( const ABC & );
Affiche( const DEF & );
Affiche( const GHI & );
pareil et pire pour le calcul :
ABC CalculeABC();
DEF CalculeDEF();
GHI CalculeGHI();

Avec les templates :
ABC abc = Caclule<ABC>();
ABC def = Caclule<DEF>();

Affiche( abc );
Affiche( def );

J'ai une seule fonction calcule et affiche. Et pas possible de faire
ITriplet * abc = new DEF( 0, 0, 0 ); Le typage des triplets doit être
statique.


--
Aurélien REGAT-BARREL