Réponse courte: pour faire x
un nom dépendant, afin que la recherche soit différée jusqu'à ce que le paramètre du modèle soit connu.
Réponse longue: lorsqu'un compilateur voit un modèle, il est censé effectuer certaines vérifications immédiatement, sans voir le paramètre du modèle. D'autres sont différés jusqu'à ce que le paramètre soit connu. Cela s'appelle une compilation en deux phases, et MSVC ne le fait pas, mais il est requis par la norme et implémenté par les autres principaux compilateurs. Si vous le souhaitez, le compilateur doit compiler le modèle dès qu'il le voit (sur une sorte de représentation d'arbre d'analyse interne) et reporter la compilation de l'instanciation à plus tard.
Les vérifications effectuées sur le modèle lui-même, plutôt que sur des instanciations particulières de celui-ci, nécessitent que le compilateur soit capable de résoudre la grammaire du code dans le modèle.
En C ++ (et C), afin de résoudre la grammaire du code, vous devez parfois savoir si quelque chose est un type ou non. Par exemple:
#if WANT_POINTER
typedef int A;
#else
int A;
#endif
static const int x = 2;
template <typename T> void foo() { A *x = 0; }
si A est un type, qui déclare un pointeur (sans autre effet que de masquer le global x
). Si A est un objet, c'est une multiplication (et à moins qu'un opérateur ne surcharge, c'est illégal, l'affecter à une valeur r). Si elle est erronée, cette erreur doit être diagnostiquée dans la phase 1 , elle est définie par la norme comme une erreur dans le modèle , pas dans une instanciation particulière de celui-ci. Même si le modèle n'est jamais instancié, si A est un, int
le code ci-dessus est mal formé et doit être diagnostiqué, tout comme il ne le serait foo
pas du tout, mais une simple fonction.
Maintenant, la norme dit que les noms qui ne dépendent pas des paramètres du modèle doivent être résolus dans la phase 1. A
ici n'est pas un nom dépendant, il fait référence à la même chose quel que soit le type T
. Il doit donc être défini avant la définition du modèle afin d'être trouvé et vérifié dans la phase 1.
T::A
serait un nom qui dépend de T. Nous ne pouvons pas savoir dans la phase 1 si c'est un type ou non. Le type qui sera finalement utilisé comme T
dans une instanciation n'est probablement pas encore défini, et même s'il l'était, nous ne savons pas quel (s) type (s) sera utilisé comme paramètre de modèle. Mais nous devons résoudre la grammaire afin de faire nos précieuses vérifications de phase 1 pour les modèles mal formés. Ainsi, la norme a une règle pour les noms dépendants - le compilateur doit supposer qu'ils ne sont pas des types, sauf s'ils sont qualifiés avec typename
pour spécifier qu'ils sont des types, ou utilisés dans certains contextes non ambigus. Par exemple, dans template <typename T> struct Foo : T::A {};
, T::A
est utilisé comme classe de base et est donc sans ambiguïté un type. Si Foo
est instancié avec un type qui a un membre de donnéesA
au lieu d'un type A imbriqué, c'est une erreur dans le code faisant l'instanciation (phase 2), pas une erreur dans le modèle (phase 1).
Mais qu'en est-il d'un modèle de classe avec une classe de base dépendante?
template <typename T>
struct Foo : Bar<T> {
Foo() { A *x = 0; }
};
A est-il un nom dépendant ou non? Avec les classes de base, n'importe quel nom peut apparaître dans la classe de base. On pourrait donc dire que A est un nom dépendant et le traiter comme un non-type. Cela aurait l'effet indésirable que chaque nom dans Foo est dépendant, et donc chaque type utilisé dans Foo (sauf les types intégrés) doit être qualifié. À l'intérieur de Foo, vous devez écrire:
typename std::string s = "hello, world";
car std::string
serait un nom dépendant, et donc supposé être un non-type, sauf indication contraire. Aie!
Un deuxième problème lié à l'autorisation de votre code préféré ( return x;
) est que même s'il Bar
est défini avant Foo
et x
n'est pas membre de cette définition, quelqu'un pourrait ultérieurement définir une spécialisation de Bar
pour un certain type Baz
, tel qu'il Bar<Baz>
a un membre de données x
, puis instancier Foo<Baz>
. Ainsi, dans cette instanciation, votre modèle retournerait le membre de données au lieu de renvoyer le global x
. Ou à l'inverse, si la définition de modèle de base de Bar
had x
, ils pourraient définir une spécialisation sans elle, et votre modèle rechercherait un global x
à retourner Foo<Baz>
. Je pense que cela a été jugé tout aussi surprenant et pénible que le problème que vous avez, mais c'est silence surprenant, par opposition à jeter une erreur surprenante.
Pour éviter ces problèmes, la norme en vigueur indique que les classes de base dépendantes des modèles de classe ne sont tout simplement pas prises en compte pour la recherche, sauf demande explicite. Cela empêche tout d'être dépendant juste parce qu'il pourrait être trouvé dans une base dépendante. Cela a également l'effet indésirable que vous voyez - vous devez qualifier des éléments de la classe de base ou ils ne sont pas trouvés. Il existe trois façons courantes de rendre A
dépendant:
using Bar<T>::A;
dans la classe - A
se réfère maintenant à quelque chose en Bar<T>
, donc dépendant.
Bar<T>::A *x = 0;
au point d'utilisation - Encore une fois, A
c'est définitivement le cas Bar<T>
. Il s'agit d'une multiplication car elle typename
n'a pas été utilisée, donc peut-être un mauvais exemple, mais nous devrons attendre l'instanciation pour savoir sioperator*(Bar<T>::A, x)
renvoie une valeur r. Qui sait, peut-être que oui ...
this->A;
au point d'utilisation - A
est un membre, donc s'il n'est pas dans Foo
, il doit être dans la classe de base, encore une fois la norme dit que cela le rend dépendant.
La compilation en deux phases est difficile et difficile, et introduit des exigences surprenantes pour un verbiage supplémentaire dans votre code. Mais un peu comme la démocratie, c'est probablement la pire façon de faire les choses, à part toutes les autres.
Vous pourriez raisonnablement affirmer que dans votre exemple, cela return x;
n'a pas de sens six
s'agit d'un type imbriqué dans la classe de base, donc le langage devrait (a) dire que c'est un nom dépendant et (2) le traiter comme un non-type, et votre code fonctionnerait sans this->
. Dans une certaine mesure, vous êtes victime de dommages collatéraux de la solution à un problème qui ne s'applique pas dans votre cas, mais il y a toujours le problème de votre classe de base qui pourrait introduire sous vous des noms qui masquent les globaux, ou ne pas avoir de noms que vous pensiez ils avaient, et un être global à la place.
Vous pouvez également argumenter que la valeur par défaut devrait être l'opposé pour les noms dépendants (supposez que le type à moins qu'il ne soit spécifié d'une manière ou d'une autre comme un objet), ou que la valeur par défaut devrait être plus sensible au contexte (dans std::string s = "";
, std::string
pourrait être lu comme un type car rien d'autre ne rend grammatical sens, même si elle std::string *s = 0;
est ambiguë). Encore une fois, je ne sais pas exactement comment les règles ont été convenues. Je suppose que le nombre de pages de texte qui serait nécessaire, atténué contre la création d'un grand nombre de règles spécifiques pour quels contextes prennent un type et lequel non-type.