I. Problème▲
I-A. Question Guru▲
Y a-t-il quelque chose de mauvais dans le code ci-dessus ? Si oui, quoi ?
template
<
class
T>
struct
X_base {
typedef
T instantiated_type;
}
;
template
<
class
A, class
B>
struct
X : public
X_base<
B>
{
bool
operator
()( const
instantiated_type&
i ) {
return
( i !=
instantiated_type() );
}
// ... d'autres choses ...
}
;
II. Solution▲
Y a-t-il quelque chose de mauvais dans le code ci-dessus ? Si oui, quoi ?
Cet exemple illustre le problème de pourquoi et comment utiliser typename pour se référer à des noms dépendants et peut apporter un éclairage à la question : « qu'est-ce qu'un nom ? »
II-A. Utilisation de typename pour des noms dépendants▲
Le problème avec X est que instantiated_type est censé se référer au typedef supposé hérité de la classe de base X_base<B>. Malheureusement, au moment où le compilateur doit analyser la définition inline de X<A,B>::operator()(), les noms dépendants (c'est-à-dire les noms qui dépendent des paramètres de template, comme le nom hérité X_Base::instantiated_type) ne sont pas visibles, aussi le compilateur se plaindra-t-il de ne pas savoir ce que instantiated_type est censé signifier. Les noms dépendants n'apparaîtront que plus tard, au moment où le template sera effectivement instancié.
Si vous vous demandez pourquoi le compilateur n'a pas pu comprendre, imaginez que vous êtes un compilateur et demandez-vous comment comprendre ce que instantiated_type signifie ici. À la dernière ligne, vous ne pouvez pas comprendre, parce que vous ne savez pas encore ce qu'est B, ni si plus tard il n'y aura pas une spécialisation pour X_base<B> qui ne fera pas de X_base<B>::instantiated_type quelque chose d'inattendu – un nom de type ou même une variable membre. Dans le template non spécialisé X_base ci-dessus, X_base<T>::instantiated_type sera toujours T, mais rien ne peut empêcher quelqu'un de changer cela pour une spécialisation, par exemple :
template
<>
struct
X_base<
int
>
{
typedef
Y instantiated_type;
}
;
Accepté, le nom du typedef induirait un peu en erreur, mais c'est légal. Ou même :
template
<>
struct
X_base<
double
>
{
double
instantiated_type;
}
;
À présent, le nom induit moins en erreur, mais le template X ne peut pas fonctionner avec X_base<double> comme une classe de base parce que instantiated_type est une variable membre, pas un nom de type.
À la dernière ligne, le compilateur ne saura pas comment analyser la définition de X<A,B>::operator()() à moins que nous disions ce qu'est instantiated_type… au minimum si c'est un type ou autre chose. Ici, nous voulons que ce soit un type.
La façon de dire au compilateur que quelque chose comme cela est censé être un nom de type consiste à entrer le mot-clé typename. Pour nous, il y a deux façons de s'y prendre : la moins élégante consiste à simplement écrire typename partout où nous nous référons au instantiated_type :
template
<
class
A, class
B>
struct
X : public
X_base<
B>
{
bool
operator
() ( const
typename
X_base<
B>
::
instantiated_type&
i ) {
return
( i !=
typename
X_base<
B>
::
instantiated_type() );
}
// ... d'autres choses ...
}
;
J'espère que vous avez tressailli en lisant cela. Comme d'habitude, les typedef rendent ce genre de choses beaucoup plus lisibles et en fournissant un autre typedef, le reste de la définition fonctionne comme il a été écrit à l'origine :
template
<
class
A, class
B>
struct
X : public
X_base<
B>
{
typedef
typename
X_base<
B>
::
instantiated_type instantiated_type;
bool
operator
()( const
instantiated_type&
i ) {
return
( i !=
instantiated_type() );
}
// ... d'autres choses ...
}
;
Avant d'aller plus loin dans la lecture, y a-t-il quoi que ce soit qui vous paraisse inhabituel concernant l'ajout de ce typedef ?
II-B. Le point secondaire (et subtil)▲
J'aurais pu utiliser des exemples plus simples pour illustrer cela (plusieurs apparaissent dans la norme au chapitre 14.6.2), mais cela n'aurait pas fait ressortir la chose inhabituelle : la seule raison d'être de la base vide X_base semble être de fournir le typedef. Toutefois, les classes dérivées finissent habituellement simplement par un nouveau typedef.
Cela ne semble-t-il pas redondant ? Ça l'est, mais seulement un petit peu… Après tout, c'est toujours la spécialisation de X_base<> qui est responsable de la détermination de ce que le type approprié doit être, et ce type peut changer pour différentes spécialisations.
La bibliothèque standard contient des classes de base comme ceci : « bags-o-typedefs ». Elles sont destinées à être utilisées d'une seule façon. En espérant que ce sujet de GotW aidera à prévenir certaines questions concernant pourquoi des classes dérivées « re-typedef-ent » ces typedef, de façon apparemment redondante et à montrer que cet effet n'est pas vraiment une défaillance de la conception de langage et qu'il est juste une autre facette de cette question vieille comme le monde : « Qu'est-ce qu'un nom ? »
II-C. Plaisanterie▲
En bonus, voici une petite plaisanterie :
#include
<iostream>
using
std::
cout;
using
std::
endl;
struct
Rose {}
;
struct
A {
typedef
Rose rose; }
;
template
<
class
T>
struct
B : T {
typedef
typename
T::
rose foo; }
;
template
<
class
T>
void
smell( T ) {
cout <<
"awful"
<<
endl; }
void
smell( Rose ) {
cout <<
"sweet"
<<
endl; }
int
main() {
smell( A::
rose() );
smell( B<
A>
::
foo() );
}
:-)
III. Remerciements▲
Cet article est une traduction par l'équipe de la rubrique C++ de l'article de Herb Sutter publié sur Guru of the Week. Vous pouvez retrouver cet article dans sa version originale sur le site de Guru of the Week : TypenameTypename.
Merci à Luc Hermitte pour sa relecture technique et à ClaudeLELOUP pour sa relecture orthographique.