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

1 2 3 4 5
Avatar
Fabien LE LEZ
On Mon, 12 Jul 2004 11:30:44 +0200, "Aurélien REGAT-BARREL"
:

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.


Houlà... Donc, tu ne veux ni héritage, ni fonctions virtuelles, juste
t'assurer 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


C'est bien ça ?

Je pense que le mieux est de ne pas créer de code, mais juste mettre
dans la doc que la classe créée doit avoir ces fonctions.

Eventuellement, avec certaines fonctions de Boost (cherche du côté de
<http://www.boost.org/libs/libraries.htm#Correctness> ou
<http://www.boost.org/libs/libraries.htm#Generic>), tu devrais pouvoir
créer une macro "VERIFIER_APPARTENANCE_A_ITriplet(nom_classe)" qui
refuse la compilation si une des fonctions obligatoires est absente.

Avatar
Aurélien REGAT-BARREL
Houlà... Donc, tu ne veux ni héritage, ni fonctions virtuelles, juste
t'assurer 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


C'est bien ça ?


C'est tout à fait ça.

Je pense que le mieux est de ne pas créer de code, mais juste mettre
dans la doc que la classe créée doit avoir ces fonctions.


La doc se présente entre autre sous forme d'un diagramme des classes UML qui
définit cette interface. Il y aura bien un bout de texte qui va en parler,
mais je voudrais rendre mon code le plus parlant possible.

Eventuellement, avec certaines fonctions de Boost (cherche du côté de
<http://www.boost.org/libs/libraries.htm#Correctness> ou
<http://www.boost.org/libs/libraries.htm#Generic>), tu devrais pouvoir
créer une macro "VERIFIER_APPARTENANCE_A_ITriplet(nom_classe)" qui
refuse la compilation si une des fonctions obligatoires est absente.


C'était mon idée n° 3, fournir une fonction templétée de vérification.
Mais en fait y'a juste une chose qui me dérange : la moitié du code entre
ces classes est identique : accéder aux valeurs des triplets, accéder au
nom, accéder au nom de chaque triplet,... d'où mon idée n°2.
ITriplet n'est rien d'autre qu'un conteneur, seul le contenu varie entre les
différents triplets. C'est un peu comme si je voulais définir une classe
vector avec une fonction GetName() et un constructeur personnalisé...
Car en fin de compte seul le code du constructeur diffère ainsi que la
valeur des chaines renvoyées.
Je vais regarder boost. Merci.


--
Aurélien REGAT-BARREL


Avatar
Fabien LE LEZ
On Mon, 12 Jul 2004 12:13:48 +0200, "Aurélien REGAT-BARREL"
:

Mais en fait y'a juste une chose qui me dérange : la moitié du code entre
ces classes est identique : accéder aux valeurs des triplets, accéder au
nom, accéder au nom de chaque triplet,... d'où mon idée n°2.
[...]
C'est un peu comme si je voulais définir une classe
vector avec une fonction GetName() et un constructeur personnalisé...


Ben oui, en C++ y'a un outil tout fait pour ça : l'héritage ;-)

Note que si tu peux utiliser l'héritage, mais sans qu'une classe soit
"indirectement convertible" en une autre, y'a un moyen :

template <char const* nom_classe> /* Pas sûr de moi, sur ce coup-là,
mais on doit pouvoir s'en sortir d'une façon ou d'une autre... */
class ITriplet
{
public:
ITriplet (double a, double b, double c)
{ data[0]= a; data[1]= b; data[2]= c; }
static std::string GetTripletName() { return nom_classe; }
double GetValue (int index) const
{
if (index>=0 && index<NB_VALEURS) return data[i];
throw "Erreur ITriplet" + GetTripletName() + "::GetValue";
}
protected:
enum { NB_VALEURS= 3 };
double data[NB_VALEURS]; /* Pas besoin d'un vector<> si on connaît
la taille à la compilation ;-) */
};

class MontripletMachin: public ITriplet <"MontripletMachin">
{
//...
};

Avatar
Aurélien REGAT-BARREL
Ben oui, en C++ y'a un outil tout fait pour ça : l'héritage ;-)

Note que si tu peux utiliser l'héritage, mais sans qu'une classe soit
"indirectement convertible" en une autre, y'a un moyen :



C'est surtout le polymorphisme qui me gêne.

template <char const* nom_classe> /* Pas sûr de moi, sur ce coup-là,
mais on doit pouvoir s'en sortir d'une façon ou d'une autre... */
class ITriplet
{
public:
ITriplet (double a, double b, double c)
{ data[0]= a; data[1]= b; data[2]= c; }
static std::string GetTripletName() { return nom_classe; }
double GetValue (int index) const
{
if (index>=0 && index<NB_VALEURS) return data[i];
throw "Erreur ITriplet" + GetTripletName() + "::GetValue";
}
protected:
enum { NB_VALEURS= 3 };
double data[NB_VALEURS]; /* Pas besoin d'un vector<> si on connaît
la taille à la compilation ;-) */
};

class MontripletMachin: public ITriplet <"MontripletMachin">
{
//...
};


char * directement c'est pas possible, d'où le recours à un functor
GetTripletName() dans mon exemple 2.
Donc ta solution n'est pas possible. Sans compter qu'il faut aussi
customiser le constructeur, et encore d'autres trucs que j'ai pas mis
(GetValueName, ...).
Actuellement je penche pour une solution comme ceci :

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

// partie spécifique du triplet ABC
struct ABCImpl
{
static std::vector<double> Compute( 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;
}
static std::string GetName()
{
return "A.B.C";
}
};

// triplet ABC
typedef ITriplet<ABCImpl> ABC;

D'un côte le template qui définit l'interface (donc on est assez proche du
diagramme UML) + tout ce qui est redondant (stockage des triplets, accès aux
valeurs, ...) et de l'autre la partie spécifique à chaque triplet sous forme
d'une struct passée en paramètre du template ITriplet... C'est un peu le
principe du pattern façade, le template ITriplet jouant le rôle de...
l'interface.


--
Aurélien REGAT-BARREL

Avatar
Fabien LE LEZ
Deuxième essai :

template <class T> class ITriplet
{
public:
double GetValue (int index) const
{
if (index<0 || index>=NB_VALEURS)
throw "Erreur ITriplet::GetValue";
return data[index];
}
protected:
enum { NB_VALEURS= 3 };
double data[NB_VALEURS];
ITriplet (double a, double b, double c)
{ data[0]= a; data[1]= b; data[2]= c; }
};

class Essai: public ITriplet<Essai>
{
public:
Essai (double a, double b, double c)
: ITriplet<Essai> (a,b,c) {}
static char const * GetName() { return "Essai"; }
};

class Essai2: public ITriplet<Essai>
{
public:
Essai2 (double a, double b, double c)
: ITriplet<Essai> (a,b,c) {}
static char const * GetName() { return "Essai2"; }
};

Essai et Essai2 n'ont aucun parent commun.
De plus, il n'y a pas de polymorphisme et pas de virtualité.
Avatar
Fabien LE LEZ
On Mon, 12 Jul 2004 14:12:54 +0200, "Aurélien REGAT-BARREL"
:

static std::vector<double> Compute( 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;


Pas bon, puisque tu dois retaper ce code à chaque fois.
Au moins fais-en une fonction libre !

Avatar
Aurélien REGAT-BARREL
static std::vector<double> Compute( 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;


Pas bon, puisque tu dois retaper ce code à chaque fois.
Au moins fais-en une fonction libre !


Je n'ai peut être pas été clair sur ce point. Chaque triplet se calcule à
partir de A, B et C. ABC est à part car il n'a rien à calculer.
Exemple avec le triplet DEF :

// A, B, C en pourcentages
static std::vector<double> Compute( double A, double B, double C )
{
std::vector<double> result;
double total = A + B + C;
result.reserve( 3 );
result.push_back( A / total );
result.push_back( B / total );
result.push_back( C / total );
return result;
}

On comprend mieux pourquoi il faut pas confondre / mélanger les triplets...

--
Aurélien REGAT-BARREL


Avatar
Aurélien REGAT-BARREL
Deuxième essai :

template <class T> class ITriplet
{
public:
double GetValue (int index) const
{
if (index<0 || index>=NB_VALEURS)
throw "Erreur ITriplet::GetValue";
return data[index];
}
protected:
enum { NB_VALEURS= 3 };
double data[NB_VALEURS];
ITriplet (double a, double b, double c)
{ data[0]= a; data[1]= b; data[2]= c; }
};

class Essai: public ITriplet<Essai>
{
public:
Essai (double a, double b, double c)
: ITriplet<Essai> (a,b,c) {}
static char const * GetName() { return "Essai"; }
};


Ohhhhhh...
On peut faire ça en C++...
J'étais pas loin, j'avais pensé à :

// 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
class ABC : public ITriplet<ABC::Compute, ABC::GetName>
{
protected:
static std::vector<double> Compute( 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;
}
static std::string GetName()
{
return "A.B.C";
}
};

Mais c'est le serpent qui se mord la queue, j'utilise ABC pour définir ABC.
Du coup je pige pas pourquoi ton exemple fonctionne, mais je crois que c'est
pile ce qu'il me fallait. Manquerait plus qu'à comprendre comment je peux
instancier un template à partir d'un type qui n'est pas encore défini, afin
d'hériter de ce type instancié et donc finalement hériter de soit même...
|-o
Comment ça marche ?

--
Aurélien REGAT-BARREL

Avatar
Fabien LE LEZ
On Mon, 12 Jul 2004 15:16:20 +0200, "Aurélien REGAT-BARREL"
:

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).

Au début j'avais écrit

class ABC;
class ABC: public ITriplet<ABC> ...

mais finalement la forward-declaration semble inutile.

Avatar
Aurélien REGAT-BARREL
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
{
};

sauf que là ça marche... pour moi les templates ça marche un peu comme les
macros ( ~ génération de code), mais apparement c'est pas le cas.
J'ai toujours pas compris :-/

--
Aurélien REGAT-BARREL


1 2 3 4 5