web-dev-qa-db-fra.com

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

Si les classes ci-dessous n'étaient pas des modèles, je pourrais simplement avoir x dans la classe derived. 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;
}
175
Ali

Réponse courte: afin de faire de 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 (dans 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 celle-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 fonction simple.

Maintenant, la norme dit que les noms qui ne sont pas dépendants des paramètres du modèle doivent être résolvables dans la phase 1. A ici n'est pas un nom dépendant, il fait référence à la même chose indépendamment tapez 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 probablement pas savoir dans la phase 1 si c'est un type ou non. Le type qui sera éventuellement 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 types, ou utilisés dans certains sans ambiguïté contextes. 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ées A au lieu d'un type imbriqué A, c'est une erreur dans le code faisant l'instanciation (phase 2), pas une erreur dans le modèle (la 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 pourrait 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 pour 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";

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 avec 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 membre dans ce définition, quelqu'un pourrait par la suite définir une spécialisation de Bar pour certains types Baz, de telle sorte que Bar<Baz> ait 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 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 un x global à renvoyer dans 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 en silence surprenant, au lieu de lancer 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 simplement pas recherchées pour les noms, sauf si les noms sont déjà dépendants pour une autre raison. 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épendante:

  • using Bar<T>::A; Dans la classe - A fait désormais référence à quelque chose dans Bar<T>, Donc dépendant.
  • Bar<T>::A *x = 0; Au point d'utilisation - Encore une fois, A est définitivement dans Bar<T>. Il s'agit d'une multiplication car typename n'a pas été utilisé, donc 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, peut-être que oui ...
  • this->A; Au point d'utilisation - A est 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, 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 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 éventuellement faire valoir que la valeur par défaut devrait être l'opposé pour les noms dépendants (supposez que le type, sauf indication contraire comme étant 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 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 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 qui un non-type.

248
Steve Jessop

(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 à commentaire de mmichael , du projet N3337 du C++ 11 La norme:

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é au point de définition du modèle de classe ou du membre ou lors d'une instanciation du modèle de classe ou du membre.

Que "parce que la norme le dit" compte comme réponse, je ne sais pas. Nous pouvons maintenant demander pourquoi la norme exige cela, mais comme excellente réponse de Steve Jessop et d'autres le soulignent, 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.

12
Ali

x est masqué lors de 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; }
};
11
chrisaycock