Réponse courte : pour faire x
un nom dépendant, de sorte que la recherche est 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 reportées jusqu'à ce que le paramètre soit connu. C'est ce qu'on appelle la compilation en deux phases, et MSVC ne la pratique pas, mais elle est exigée par la norme et mise en œuvre par les autres principaux compilateurs. Si vous le souhaitez, le compilateur doit compiler le modèle dès qu'il le voit (vers une sorte de représentation interne de l'arbre d'analyse), et reporter la compilation de l'instanciation à plus tard.
Les contrôles effectués sur le modèle lui-même, plutôt que sur des instanciations particulières, exigent que le compilateur soit en mesure de résoudre la grammaire du code dans le modèle.
En C++ (et en C), pour résoudre la grammaire du code, il faut parfois savoir si quelque chose est un type ou non. C'est le cas 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 d'ombrer la valeur globale de x
). Si A est un objet, il s'agit d'une multiplication (et à moins d'une surcharge de l'opérateur, c'est illégal, l'assignation à une valeur r). Si c'est faux, cette erreur doit être diagnostiquée en phase 1 la norme la définit comme une erreur dans le modèle et non dans une instanciation particulière de celui-ci. Même si le modèle n'est jamais instancié, si A est un int
alors le code ci-dessus est mal formé et doit être diagnostiqué, tout comme il le serait si foo
n'était pas du tout un modèle, mais une simple fonction.
La norme stipule que les noms qui ne sont pas dépendant des paramètres du modèle doivent pouvoir être résolus en phase 1. A
n'est pas un nom dépendant, il se réfère à la même chose indépendamment du type T
. Il doit donc être défini avant la définition du modèle afin d'être trouvé et vérifié lors de la phase 1.
T::A
serait un nom qui dépend de T. Nous ne pouvons pas savoir dans la phase 1 s'il s'agit d'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(ont) utilisé(s) comme paramètre du modèle. Mais nous devons résoudre la grammaire afin d'effectuer nos précieuses vérifications de phase 1 pour les modèles mal formés. Le standard a donc une règle pour les noms dépendants : le compilateur doit supposer qu'il s'agit de non-types, à moins qu'ils ne soient qualifiés par la mention typename
pour préciser qu'ils sont 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 le Foo
est instancié avec un type qui possède un membre de données A
au lieu d'un type A imbriqué, il s'agit d'une erreur dans le code qui effectue l'instanciation (phase 2), et non d'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 de personne dépendante ou non ? Avec les classes de base, tous pourrait apparaître dans la classe de base. Nous pourrions donc dire que A est un nom dépendant et le traiter comme un non-type. Cela aurait pour effet indésirable que chaque nom dans Foo est dépendante, et donc tous les types utilisés dans Foo (à l'exception des types intégrés) doivent être qualifiés. A l'intérieur de Foo, il faudrait écrire :
typename std::string s = "hello, world";
parce que 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 si Bar
est défini avant Foo
et x
n'est pas un membre dans cette définition, quelqu'un pourrait plus tard définir une spécialisation de Bar
pour un certain type Baz
, tel que Bar<Baz>
dispose d'un membre de données x
et instanciez ensuite Foo<Baz>
. Ainsi, dans cette instanciation, votre modèle renverra le membre de données au lieu de renvoyer le membre global. x
. Ou inversement, si la définition du modèle de base de Bar
avait x
ils pourraient définir une spécialisation sans elle, et votre modèle rechercherait une valeur globale de x
pour revenir en Foo<Baz>
. Je pense que cela a été jugé tout aussi surprenant et angoissant que le problème que vous rencontrez, mais c'est en silence surprenante, par opposition au fait de commettre une erreur surprenante.
Pour éviter ces problèmes, la norme stipule en fait que les classes de base dépendantes des modèles de classe ne sont pas prises en compte pour la recherche, sauf demande explicite. Cela permet d'éviter que tout soit considéré comme dépendant simplement parce que l'on pourrait le trouver dans une base dépendante. Cela a également l'effet indésirable que vous constatez : vous devez qualifier les éléments de la classe de base, sinon ils ne sont pas trouvés. Il y a trois façons courantes de rendre A
dépendante :
-
using Bar<T>::A;
dans la classe - A
se réfère maintenant à quelque chose dans Bar<T>
donc dépendante.
-
Bar<T>::A *x = 0;
au point d'utilisation - Encore une fois, A
est définitivement en Bar<T>
. Il s'agit d'une multiplication puisque typename
n'a pas été utilisé, ce qui est peut-être un mauvais exemple, mais nous devrons attendre l'instanciation pour savoir si operator*(Bar<T>::A, x)
renvoie une valeur r. Qui sait, c'est peut-être le cas...
-
this->A;
au point d'utilisation - A
est un membre, donc s'il n'est pas dans Foo
Il doit se trouver dans la classe de base, ce qui, selon la norme, le rend dépendant.
La compilation en deux phases est délicate et difficile, et introduit des exigences surprenantes en termes de verbiage supplémentaire dans votre code. Mais, à l'instar de la démocratie, c'est probablement la pire façon de faire les choses, à l'exception de toutes les autres.
On peut raisonnablement penser que c'est le cas dans votre exemple, return x;
n'a pas de sens si x
est 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 dus à la solution d'un problème qui ne s'applique pas à votre cas, mais il y a toujours le problème de votre classe de base qui introduit potentiellement des noms sous vous qui font de l'ombre aux globaux, ou qui n'ont pas les noms que vous pensiez qu'ils avaient, et un global est trouvé à la place.
Vous pourriez également soutenir que la valeur par défaut devrait être l'inverse pour les noms dépendants (supposer un type à moins qu'il ne soit spécifié d'une manière ou d'une autre qu'il s'agit d'un objet), ou que la valeur par défaut devrait être plus sensible au contexte (en std::string s = "";
, std::string
pourrait être lu comme un type puisque rien d'autre n'a de sens grammatical, même si std::string *s = 0;
est ambiguë). Encore une fois, je ne sais pas exactement comment les règles ont été convenues. Je pense que le nombre de pages de texte qui aurait été nécessaire a permis d'éviter la création d'un grand nombre de règles spécifiques concernant les contextes qui prennent un type et ceux qui n'en prennent pas.
1 votes
Ah jeez. Cela a quelque chose à voir avec la recherche de noms. Si quelqu'un ne répond pas à cette question rapidement, je la chercherai et la posterai (je suis occupé pour l'instant).
0 votes
@Ed Swangren : Désolé, je l'ai oublié parmi les réponses proposées lorsque j'ai posté cette question. Je cherchais la réponse depuis longtemps déjà.
7 votes
Cela est dû à la recherche de noms en deux phases (que tous les compilateurs n'utilisent pas par défaut) et aux noms dépendants. Il y a 3 solutions à ce problème, autres que de préfixer le nom de l'élément
x
avecthis->
, à savoir : 1) Utiliser le préfixebase<T>::x
, 2) Ajouter une déclarationusing base<T>::x
, 3) Utilisez un commutateur de compilation global qui active le mode permissif. Les avantages et inconvénients de ces solutions sont décrits dans le document stackoverflow.com/questions/50321788/