Pourquoi dois-je accéder aux membres de la classe de base du modèle via le pointeur this?

199

Si les classes ci-dessous n'étaient pas des modèles, je pourrais simplement en avoir xdans la derivedclasse. Cependant, avec le code ci-dessous, je dois utiliser this->x. Pourquoi?

template <typename T>
class base {

protected:
    int x;
};

template <typename T>
class derived : public base<T> {

public:
    int f() { return this->x; }
};

int main() {
    derived<int> d;
    d.f();
    return 0;
}
Ali
la source
1
Ah jeez. Cela a quelque chose à voir avec la recherche de nom. Si quelqu'un ne répond pas rapidement, je vais le rechercher et le poster (occupé maintenant).
templatetypedef
@Ed Swangren: Désolé, je l'ai manqué parmi les réponses proposées lors de la publication de cette question. Je cherchais la réponse depuis longtemps.
Ali
6
Cela se produit en raison de la recherche de nom en deux phases (que tous les compilateurs n'utilisent pas par défaut) et des noms dépendants. Il existe 3 solutions à ce problème, autres que le préfixe xavec this->, à savoir: 1) Utilisez le préfixe base<T>::x, 2) Ajoutez une instruction using base<T>::x, 3) Utilisez un commutateur de compilation global qui active le mode permissif. Les avantages et les inconvénients de ces solutions sont décrits dans stackoverflow.com/questions/50321788/…
George Robinson

Réponses:

274

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, intle code ci-dessus est mal formé et doit être diagnostiqué, tout comme il ne le serait foopas 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. Aici 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::Aserait 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 Tdans 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 typenamepour 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::Aest utilisé comme classe de base et est donc sans ambiguïté un type. Si Fooest 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::stringserait 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 Barest défini avant Fooet xn'est pas membre de cette définition, quelqu'un pourrait ultérieurement définir une spécialisation de Barpour 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 Barhad 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 Adépendant:

  • using Bar<T>::A;dans la classe - Ase réfère maintenant à quelque chose en Bar<T>, donc dépendant.
  • Bar<T>::A *x = 0;au point d'utilisation - Encore une fois, Ac'est définitivement le cas Bar<T>. Il s'agit d'une multiplication car elle typenamen'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 - Aest 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::stringpourrait ê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.

Steve Jessop
la source
1
Ooh, belle réponse détaillée. Clarifié deux ou trois choses que je n'ai jamais pris la peine de rechercher. :) +1
jalf
20
@jalf: existe-t-il quelque chose comme le C ++ QTWBFAETYNSYEWTKTAAHMITTBGOW - "Questions qui seraient fréquemment posées, sauf que vous n'êtes même pas sûr de vouloir connaître la réponse et d'avoir des choses plus importantes à faire"?
Steve Jessop
4
réponse extraordinaire, je me demande si la question pourrait entrer dans la FAQ.
Matthieu M.
Whoa, peut-on dire encyclopédique? highfive Un point subtil, cependant: "Si Foo est instancié avec un type qui a un membre de données A 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)." Il serait peut-être préférable de dire que le modèle n'est pas mal formé, mais cela pourrait toujours être le cas d'une hypothèse incorrecte ou d'un bug logique de la part de l'auteur du modèle. Si l'instanciation signalée était en fait le cas d'utilisation prévu, le modèle serait incorrect.
Ionoclast Brigham
1
@JohnH. Étant donné que plusieurs compilateurs implémentent -fpermissiveou similaire, oui c'est possible. Je ne connais pas les détails de la façon dont il est implémenté, mais le compilateur doit différer la résolution xjusqu'à ce qu'il connaisse la classe de base tempate réelle T. Donc, en principe, en mode non permissif, il peut enregistrer le fait qu'il l'a différé, le reporter, faire la recherche une fois qu'il l'a fait T, et lorsque la recherche réussit, émettez le texte que vous proposez. Ce serait une suggestion très précise si elle n'est faite que dans les cas où cela fonctionne: les chances que l'utilisateur en ait voulu un autre xd'une autre portée sont assez minuscules!
Steve Jessop
13

(Réponse originale du 10 janvier 2011)

Je pense avoir trouvé la réponse: problème GCC: utilisation d'un membre d'une classe de base qui dépend d'un argument de modèle . La réponse n'est pas spécifique à gcc.


Mise à jour: En réponse au commentaire de mmichael , du projet N3337 de la norme C ++ 11:

14.6.2 Noms dépendants [temp.dep]
[...]
3 Dans la définition d'une classe ou d'un modèle de classe, si une classe de base dépend d'un paramètre de modèle, la portée de la classe de base n'est pas examinée lors de la recherche de nom non qualifié à le point de définition du modèle ou membre de classe ou lors d'une instanciation du modèle ou membre de classe.

Que "parce que la norme le dit" compte comme réponse, je ne sais pas. Nous pouvons maintenant demander pourquoi la norme l'exige, mais comme le soulignent l'excellente réponse de Steve Jessop et d'autres, la réponse à cette dernière question est plutôt longue et discutable. Malheureusement, en ce qui concerne la norme C ++, il est souvent presque impossible de donner une explication courte et autonome des raisons pour lesquelles la norme impose quelque chose; cela vaut également pour cette dernière question.

Ali
la source
11

Le xest caché pendant l'héritage. Vous pouvez afficher via:

template <typename T>
class derived : public base<T> {

public:
    using base<T>::x;             // added "using" statement
    int f() { return x; }
};
chrisaycock
la source
25
Cette réponse n'explique pas pourquoi elle est cachée.
jamesdlin