232 votes

Pourquoi dois-je accéder aux membres de la classe de base du modèle par l'intermédiaire du pointeur this ?

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

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 avec this-> , à savoir : 1) Utiliser le préfixe base<T>::x , 2) Ajouter une déclaration using 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/

319voto

Steve Jessop Points 166970

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.

2 votes

Ooh, belle réponse détaillée. J'ai clarifié quelques points que je n'avais jamais pris la peine de vérifier :) +1

25 votes

@jalf : existe-t-il une chose telle que le C++QTWBFAETYNSYEWTKTAAHMITTBGOW - "Questions qui seraient fréquemment posées sauf que vous n'êtes pas sûr de vouloir connaître la réponse et que vous avez des choses plus importantes à faire" ?

7 votes

Réponse extraordinaire, je me demande si la question ne pourrait pas être posée dans la faq.

15voto

Ali Points 18740

(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 , de la 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 templa la portée de la classe de base n'est pas examinée lors de l'utilisation d'un nom non qualifié. soit au moment de la définition du modèle ou du membre de la classe, soit lors d'une recherche instantanée de nom non qualifié. ou du membre, ni lors de l'instanciation du modèle de classe ou du membre.

Si "parce que la norme le dit compte comme une réponse, je ne sais pas. Nous pouvons maintenant nous demander pourquoi la norme impose cela, mais en tant que L'excellente réponse de Steve Jessop et d'autres, la réponse à cette dernière question est assez longue et discutable. Malheureusement, lorsqu'il s'agit de la norme C++, il est souvent presque impossible de donner une explication courte et complète des raisons pour lesquelles la norme impose quelque chose ; cela s'applique également à la dernière question.

12voto

chrisaycock Points 12900

En x est caché lors de l'héritage. Vous pouvez l'effacer via :

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

public:
    using base<T>::x;             // added "using" statement
    int f() { return x; }
};

28 votes

Cette réponse n'explique pas pourquoi il est caché.

0 votes

J'obtiens base<T> is not a namespace or unscoped enum

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X