OVH Cloud OVH Cloud

héritages de types définis dans une classe parente

4 réponses
Avatar
meow
Bonjour, je suis en train d'essayer de comprendre comment fonctionne
l'h=E9ritage des typedefs et autres types d=E9finis dans une classe
parente, en particulier dans le cas des templates :
Prenons le cas simple suivant :

template<class B>
struct A {
typedef int I;
I i;
A(I ii):i(ii){}
A(){}
};

template<class B>
struct AA: public A<B> {
AA(I ii){i=3Dii;}
};

int main () {
AA<float> a(2);
std::cout<<a.i;
return(0);
}

Si je supprimes les deux lignes "template<class B>", tout fonctionne,
le "type" I d=E9fini dans A est h=E9rit=E9 dans AA. Si je laisse les
templates, l=E0 =E7a coince...

16: error: expected `)' before 'ii'
In function 'int main()':
20: error: no matching function for call to 'AA<float>::AA(int)'
15: note: candidates are: AA<float>::AA()
15: note: AA<float>::AA(const AA<float>&)

Je ne comprends pas grand chose =E0 ces messages d'erreur, mais je pense
comprendre que dans la mesure o=F9 B n'est pas instanci=E9, A<B> ne l'est
pas non plus, et de fait A<B>::I non plus...
Y a t'il une solution simple et =E9l=E9gante pour n'avoir pas =E0 =E0
red=E9finir explicitement=20
"typedef typename A<B>::I I"
dans AA<B> ?

4 réponses

Avatar
Jean-Marc Bourguet
"meow" writes:

Y a t'il une solution simple et élégante pour n'avoir pas à à
redéfinir explicitement
"typedef typename A<B>::I I"
dans AA<B> ?


Non.

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org

Avatar
kanze
meow wrote:
Bonjour, je suis en train d'essayer de comprendre comment
fonctionne l'héritage des typedefs et autres types définis
dans une classe parente, en particulier dans le cas des
templates :
Prenons le cas simple suivant :

template<class B>
struct A {
typedef int I;
I i;
A(I ii):i(ii){}
A(){}
};

template<class B>
struct AA: public A<B> {
AA(I ii){i=ii;}
};

int main () {
AA<float> a(2);
std::cout<<a.i;
return(0);
}

Si je supprimes les deux lignes "template<class B>", tout
fonctionne, le "type" I défini dans A est hérité dans AA. Si
je laisse les templates, là ça coince...


Bienvenu au monde des templates:-).

16: error: expected `)' before 'ii'
In function 'int main()':
20: error: no matching function for call to 'AA<float>::AA(int)'
15: note: candidates are: AA<float>::AA()
15: note: AA<float>::AA(const AA<float>&)

Je ne comprends pas grand chose à ces messages d'erreur,


Une des caractèristiques des templates, c'est de générer des
messages d'erreur incompréhensible. Ici, ce n'est que le premier
qui est intéressant ; apparamment, le compilateur a commencé à
parser la ligne 16 comme une déclaration de la variable I, c-à-d
comme si elle était « A (I) ». Et pourquoi pas -- l'important,
ici, c'est bien que I n'est pas connu. (Et la grammaire des
déclarations en C++ est telle que quand tu utilises un symbole
inconnu, c'est souvent difficile à donner un message cohérent.
Comment parser la declaration dépend de si I est le nom d'un
type ou non, et si le symbole n'est pas connu, comment le
savoir.) Les erreurs suivantes sont toutes dues à ce que suite
la première erreur, le compilateur n'a pas pû déclaré le
constructeur. (Aussi, il a apparamment sauter le corps du
constructeur, sans doute parce qu'il n'arrivait pas à savoir ce
que faisait un {...} là. Sinon, il aurait aussi râlé qu'il ne
trouvait pas de symbole « i » non plus.)

En fin de compte, je crois que j'aurais préféré une syntaxe un
peu plus explicite. Quelque chose du genre :

template< typename B >
dependant i, typename I
class AA ...

Ce n'est pas toujours évident ce que le compilateur considère
dépendant ou non, et c'est encore moins évident, en général,
comment rendre un symbole dépendant, si c'est ça qu'on veut.

mais je pense comprendre que dans la mesure où B n'est pas
instancié, A<B> ne l'est pas non plus, et de fait A<B>::I non
plus...


Oui et non. Le problème, c'est que le symbole I n'est pas
dépendant, et que donc, le compilateur le cherche à la
définition du template, avant toute instantiation. Et qu'à ce
moment, il n'a pas accès à la classe de base, parce qu'elle est
dépendante.

Y a t'il une solution simple et élégante pour n'avoir pas à à
redéfinir explicitement
"typedef typename A<B>::I I"
dans AA<B> ?


Pas vraiement. D'une façon à une autre, il faut bien 1) faire
comprendre au compilateur que c'est un nom dépendant, et que
donc, c'est normal qu'il ne le trouve pas avant l'instantiation
du template, et 2) dire au compilateur que le symbole est le nom
d'un type.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34

Avatar
meow
Je ne comprends pas exactement ce que tu entends par "dépendant". Peux
tu expliciter s'il te plait ? Dépependant de quoi ? Du(des)
parametre(s) template ?
"comment rendre un symbole *dépendant*, si c'est ça qu'on veut."
"il n'a pas accès à la classe de base, parce qu'elle est
*dépendante*."

Sinon, dans un premier temps je me suis dit qu'il suffirait peut etre
de declarer l'existence de I dans AA<B> par un simple "typename I;" qui
serait complété par dérivation à l'instanciation de B (ce que tu
appelles définition ?). Mais je me suis fait insulter par le compilo :
expected nested-name-specifier before 'I'
gcc connait toujours plus de gros mots que moi... ça veut dire quoi,
ça, en l'occurrence ?

Après quoi je me suis dit que bon, j'étais un brin obligé de
redéfinir explicitement I dans AA<B>, par un moche "typedef typename
A<B>::I I;"
qui m'a amené à comprendre que "i" aussi subissait l'ostracisme du
compilo...
Bref, me voilà à écrire des chose aussi laides que "A<B>::i" dans
AA<B>... Après tout pourquoi pas !

Autre remarque : j'ai bien l'impression que tout ça dépend du compilo
lui meme, voire de la version dudit compilo... En fait j'ai eu un
déboire avec ma debian la semaine dernière et un certain nombre de
trucs ont été remplacés dans mon dos (je suspecte le package g++
d'en avoir fait les frais). Depuis, un certain nombre de mes classes
templates reffusent la compilation. En particulier je me suis rendu
compte aujourd'hui que:

template <class T>
class Tri : public Triplet<T,T,T>
{
[..]
T& operator[](int i){
assert((i>=0) && (i<3));
switch (i) {
case 0:return first;
case 1:return second;
case 2:return third;
}
}
[..]
};

ne tombe en marche que si je remplace par exemple "first" par
"Triplet<T,T,T>::first" !
Avatar
kanze
meow wrote:
Je ne comprends pas exactement ce que tu entends par
"dépendant". Peux tu expliciter s'il te plait ? Dépependant de
quoi ? Du(des) parametre(s) template ?


C'est un concept introduit par la norme. Grosso modo, un
compilateur doit parser un template deux fois, une première fois
lorsqu'il voit la définition, et une deuxième fois lors de
l'instantiation. Chaque fois, il doit chercher les symboles ;
sans savoir si un symbole est un nom d'un type ou non, c'est
impossible de parser le C++. C'est ce qu'on appelle la récherche
à deux phases (« two phase lookup » en anglais).

Le problème, évidemment, c'est qu'il y a des noms qu'il ne
pourrait jamais trouver dans la première recherche, parce qu'ils
dépendent des paramètres d'instantiation. Alors, la norme divise
les symboles (dans un template) en trois groupes : les noms
déclarés localement, les noms dépendants, et les noms non
dépendants. Avec des règles de récherche différentes dans chaque
cas. Grosso modo, un nom dépendant ne serait lié (« bound ») à
une déclaration que lors de l'instantiation, en prenant en
compte les noms visibles au point d'instantiation, tandis qu'un
nom non dépendant sera lié une fois pour toute lorsque le
compilateur lit la définition du template.

Ça a aussi l'avantage d'éviter le « détournement » des noms.
Supposons que j'écris un template :

#include <math.h>

template< typename Base >
class C : public Base
{
public:
double sinValue() const
{
double d = this->value() ;
return sin( d ) ;
}
} ;

C'est assez clair que la fonction sin que je veux, c'est la
fonction globale dans math.h. Mais qu'est-ce qui se passe si par
hazard, la classe Base d'instantiation contient une fonction
membre qui s'appelle sin ? Avec la recherche à deux phases,
telle que c'est défini dans la norme, sin est un nom non
dépendant ; il ne serait rechercher que lors de la définition du
template, et serait lié alors une fois pour toute à la
déclaration de la fonction globale déclarée dans math.h. Et on
utilise le this-> avant value() pour que la même chose ne se
passe pas avec la fonction value(), dont on veut que le
compilateur fasse la récherche dans la classe de base --
l'utilisation de this->, ici, rend le nom dépendant.

Pour compléter, je reviens sur le problème que j'ai cité au
début : il faut que le compilateur sache si un nom désigne un
type ou non s'il veut parser correctement. Dans le cas d'un nom
dépendant, il ne peut pas le savoir, au moins dans la première
parse. Il faut donc le lui dire, au moyen de typename.

Maintenant, tu vas me dire que dans le C++ moderne, c'est
<cmath> qu'on écrirait, et non <math.h>, et que la fonction
s'appelle std::sin, et que la qualification du nom enlève toute
ambigüité, et que même avant, si on voulait se protéger, on
aurait pû écrire ::sin, à la place de sin. Et que donc, les
avantages de toutes ces complications est en somme assez
maigres. Je n'en disconviens pas ; je te raconte simplement
comment c'est, et les arguments qu'on m'a raconté (et qui ne me
convaincs pas non plus).

"comment rendre un symbole *dépendant*, si c'est ça qu'on
veut."


Si, quand on écrit le template, on veut que le nom soit lié à
quelque chose qui ne serait connue que lors de l'instantiation,
par exemple, parce qu'il dépend des paramètres, il faut le
rendre dépendant, afin que le compilateur ne le recherche pas
toute suite, et ne le lie pas une fois pour tout. Comment ce
faire dépend du symbol, et où on s'attend à la trouver dans
l'instantiation.

"il n'a pas accès à la classe de base, parce qu'elle
est *dépendante*."


Dans ton exemple, la classe de base était dépendant, c-à-d
qu'elle dépendait d'un ou plusieurs paramètres d'instantiation.
Du coup, elle n'est pas prise en compte dans la récherche des
noms non dépendants. C'est comme dans mon exemple ci-dessus :
sin n'est pas dépendant, et même s'il est défini dans la classe
de base, dans une instantiation donnée, on ne prend pas cette
définition en compte.

Évidemment, si j'avais oublié le this-> sur value(), elle ne
serait pas dépendant non plus -- le compilateur l'aurait cheré
lors de la définition du template (et uniquement alors), ne
l'aurait pas trouvé, et aurait signalé une erreur.

Sinon, dans un premier temps je me suis dit qu'il suffirait
peut etre de declarer l'existence de I dans AA<B> par un
simple "typename I;"


Illégal. La qualification « typename » ne peut s'appliquer qu'à
un nom qualifié.

qui serait complété par dérivation à
l'instanciation de B (ce que tu appelles définition ?). Mais
je me suis fait insulter par le compilo : expected
nested-name-specifier before 'I'


C'est la règle. Ne me démande pas pourquoi, mais c'est comme ça.

gcc connait toujours plus de gros mots que moi... ça veut dire
quoi, ça, en l'occurrence ?


Par nested-name-specifier, je suppose qu'il veut dire la même
chose que moi quand j'ai dit « nom qualifié ». C-à-d que le nom
est précédé qu'une qualification de portée, du genre A::B.

Après quoi je me suis dit que bon, j'étais un brin obligé de
redéfinir explicitement I dans AA<B>, par un moche "typedef
typename A<B>::I I;"
qui m'a amené à comprendre que "i" aussi subissait
l'ostracisme du compilo...
Bref, me voilà à écrire des chose aussi laides que "A<B>::i"
dans AA<B>... Après tout pourquoi pas !


En général, même dans les premières implémentations de templates
(qui ne connaissaient pas la récherche à deux phases), c'était
bon ton de qualifier tous les noms, justement pour éviter les
malentendus. Écrire A<B>::i dit bien que tu veux le i dans
A<B>, et non n'importe quel i qui était par hazard visible.
(Typiquement, c'était plus important dans l'autre sens. Tu
écrivais ::sin, pour être sûr de ne pas chopper le sin membre
de la classe de base.)

Il y a deux façons assez répandues pour rendre un nom dépendant :
si le nom qu'on veut doit se trouver dans une classe de base (et
ce n'est pas un nom de type), on écrit this-> devant. Sinon, il
faut le qualifier avec un type qui est dépendant, A<B>::i, par
exemple. (Pour un appel de fonction, le nom de la fonction est
dépendant dès qu'un des paramètres est dépendant, à condition
qu'il ne soit pas qualifié.)

Dans ton cas, j'aurais plutôt tendance à écrire :
typedef A<B> Base ;
en tête, et utiliser Base::i partout. Ça me semble plus parlant,
et si à la place de A<B>, on avait quelque chose de plus
compliqué, c'est aussi plus facile à écrire.

Autre remarque : j'ai bien l'impression que tout ça dépend du
compilo lui meme, voire de la version dudit compilo...


Tout à fait. Les compilateurs n'ont pas attendu la norme pour
implémenter des templates, et chacun l'a fait de sa façon. Et
l'implémentation complète est assez compliquée, de façon
qu'aujourd'hui, il n'y a que EDG (donc, Comeau et Intel, parmi
d'autres) qui est à jour. Alors, chaque compilateur est à un
stade différent, et avec des adaptations plus ou moins variables
pour supporter le code existant. Et évidemment, chaque nouvelle
version cherche à être plus conforme.

En fait j'ai eu un déboire avec ma debian la semaine dernière
et un certain nombre de trucs ont été remplacés dans mon dos
(je suspecte le package g++ d'en avoir fait les frais).
Depuis, un certain nombre de mes classes templates reffusent
la compilation.


C'est connu, g++ est assez intransigeant vis-à-vis de l'ancien
code. Ce n'est pas parce qu'il a compilé avec un ancien
compilateur qu'ils se sentent obligé à le supporter. (Mais je
crois que c'est surtout un problèmes de moyens -- supporter
plusieurs modèles d'instantiation des templates exige beaucoup
d'efforts.)

Ce qu'il faut dire, c'est que chez nous, prèsqu'aucun template
qui se compilaient avec g++ 2.95.2 ou Sun CC 4.2 passe avec g++
4.0. En général, un petit coup de this-> ou un typename en plus
suffisait pour faire l'affaire, mais dans un cas, on a été
obligé à réstructurer le template. (C'était un cas avec des
dépendances réciproques, et la réstructuration a sans doute
donnée quelque chose de plus propre. Mais c'est une mise à jour
dont on se serait volentiers passé, parce que la module même est
appelée à disparaître sous peu.)

En particulier je me suis rendu compte aujourd'hui que:

template <class T>
class Tri : public Triplet<T,T,T>
{
[..]
T& operator[](int i){
assert((i>=0) && (i<3));
switch (i) {
case 0:return first;
case 1:return second;
case 2:return third;
}
}
[..]
};

ne tombe en marche que si je remplace par exemple "first" par
"Triplet<T,T,T>::first" !


Ou avec un typedef pour Triplet<T,T,T>. Ou en écrivant
this->first.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34