Por que preciso acessar os membros da class base do modelo por meio desse ponteiro?

Se as classs abaixo não fossem modelos, eu poderia simplesmente ter x na class derived . No entanto, com o código abaixo, eu tenho que usar this->x . Por quê?

 template  class base { protected: int x; }; template  class derived : public base { public: int f() { return this->x; } }; int main() { derived d; df(); return 0; } 

Resposta curta: para tornar x um nome dependente, para que a pesquisa seja adiada até que o parâmetro do modelo seja conhecido.

Resposta longa: quando um compilador vê um modelo, ele deve executar determinadas verificações imediatamente, sem ver o parâmetro do modelo. Outros são adiados até que o parâmetro seja conhecido. É chamado de compilation de duas fases e o MSVC não faz isso, mas é exigido pelo padrão e implementado pelos outros principais compiladores. Se você quiser, o compilador deve compilar o modelo assim que ele for visto (para algum tipo de representação de tree de análise interna) e adiar a compilation da instanciação até mais tarde.

As verificações que são executadas no próprio modelo, em vez de em instanciações particulares dele, exigem que o compilador seja capaz de resolver a gramática do código no modelo.

Em C ++ (e C), para resolver a gramática do código, às vezes você precisa saber se algo é um tipo ou não. Por exemplo:

 #if WANT_POINTER typedef int A; #else int A; #endif static const int x = 2; template  void foo() { A *x = 0; } 

se A é um tipo, isso declara um ponteiro (sem efeito além de sombrear o x global). Se A é um object, isso é multiplicação (e a restrição de alguma sobrecarga de operador é ilegal, atribuindo a um rvalue). Se estiver errado, este erro deve ser diagnosticado na fase 1 , é definido pelo padrão como sendo um erro no modelo , e não em alguma instanciação específica dele. Mesmo que o template nunca seja instanciado, se A for int então o código acima está mal formado e deve ser diagnosticado, assim como seria se foo não fosse um template, mas uma function simples.

Agora, o padrão diz que os nomes que não são dependentes dos parâmetros do modelo devem ser resolvidos na fase 1. A aqui não é um nome dependente, ele se refere à mesma coisa, independentemente do tipo T Por isso, precisa ser definido antes de o modelo ser definido para ser encontrado e verificado na fase 1.

T::A seria um nome que depende de T. Não podemos saber na fase 1 se é um tipo ou não. O tipo que eventualmente será usado como T em uma instanciação provavelmente ainda não está definido, e mesmo se fosse não sabemos qual tipo (s) será usado como nosso parâmetro de template. Mas temos que resolver a gramática para fazer nossas preciosas verificações de fase 1 para modelos mal formados. Portanto, o padrão tem uma regra para nomes dependentes – o compilador deve assumir que eles não são tipos, a menos que sejam qualificados com typename para especificar que são tipos ou usados ​​em determinados contextos não ambíguos. Por exemplo, no template struct Foo : T::A {}; , T::A é usado como uma class base e, portanto, é inequivocamente um tipo. Se Foo for instanciado com algum tipo que tenha um membro de dados A vez de um tipo nested A, isso é um erro no código que faz a instanciação (fase 2), não um erro no modelo (fase 1).

Mas e um modelo de class com uma class base dependente?

 template  struct Foo : Bar { Foo() { A *x = 0; } }; 

É um nome dependente ou não? Com as classs base, qualquer nome poderia aparecer na class base. Assim, poderíamos dizer que A é um nome dependente e tratá-lo como um não-tipo. Isso teria o efeito indesejável de que todo nome em Foo é dependente e, portanto, todo tipo usado em Foo (exceto tipos internos) deve ser qualificado. Dentro do Foo, você teria que escrever:

 typename std::string s = "hello, world"; 

porque std::string seria um nome dependente e, portanto, assumido como não-tipo, a menos que especificado de outra forma. Ai!

Um segundo problema em permitir seu código preferido ( return x; ) é que mesmo que Bar seja definido antes de Foo , e x não seja um membro nessa definição, alguém poderia definir uma especialização de Bar para algum tipo Baz , de tal forma que Bar tem um membro de dados x e, em seguida, instancia Foo . Portanto, nessa instanciação, seu modelo retornaria o membro de dados em vez de retornar o x global. Ou, inversamente, se a definição do modelo base de Bar tivesse x , eles poderiam definir uma especialização sem ela, e seu modelo procuraria um x global para retornar em Foo . Acho que isso foi considerado tão surpreendente e angustiante quanto o problema que você tem, mas é silenciosamente surpreendente, em vez de lançar um erro surpreendente.

Para evitar esses problemas, o padrão, na verdade, diz que as classs base dependentes de modelos de classs simplesmente não são pesquisadas por nomes, a menos que os nomes já sejam dependentes por algum outro motivo. Isso impede que tudo seja dependente apenas porque pode ser encontrado em uma base dependente. Ele também tem o efeito indesejável que você está vendo – você tem que qualificar as coisas da class base ou não é encontrado. Existem três maneiras comuns de tornar A dependente:

  • using Bar::A; na class – A agora se refere a algo no Bar , portanto dependente.
  • Bar::A *x = 0; no ponto de uso – Novamente, A está definitivamente em Bar . Isto é multiplicação já que typename não foi usado, então possivelmente um mau exemplo, mas nós teremos que esperar até a instanciação para descobrir se o operator*(Bar::A, x) retorna um rvalue. Quem sabe, talvez isso …
  • this->A; no ponto de uso – A é um membro, portanto, se não estiver no Foo , ele deve estar na class base, novamente o padrão diz que isso o torna dependente.

A compilation de duas fases é complicada e difícil, e introduz alguns requisitos surpreendentes para o palavreado extra no seu código. Mas um pouco como a democracia é provavelmente a pior maneira possível de fazer as coisas, além de todas as outras.

Você poderia razoavelmente argumentar que no seu exemplo, return x; não faz sentido se x for um tipo nested na class base, então o idioma deve (a) dizer que é um nome dependente e (2) tratá-lo como um não-tipo, e seu código funcionaria sem this-> . Até certo ponto, você é vítima de danos colaterais da solução de um problema que não se aplica ao seu caso, mas ainda há a questão de sua class base potencialmente introduzir nomes em você que são globals de sombra ou não ter nomes que você pensou eles tinham, e um ser global encontrado em seu lugar.

Você também pode possivelmente argumentar que o padrão deve ser o oposto para nomes dependentes (tipo assumir, a menos que seja especificado de alguma forma como um object), ou que o padrão deve ser mais sensível ao contexto (em std::string s = ""; std::string pode ser lido como um tipo, já que nada mais faz sentido gramatical, mesmo que std::string *s = 0; seja ambíguo). Mais uma vez, não sei bem como as regras foram acordadas. Meu palpite é que o número de páginas de texto que seriam necessárias, mitigado contra a criação de um monte de regras específicas para os contextos que tomam um tipo e que não é um tipo.

O x está oculto durante a inheritance. Você pode reexibir via:

 template  class derived : public base { public: using base::x; // added "using" statement int f() { return x; } }; 

(Resposta original de 10 de janeiro de 2011)

Acho que encontrei a resposta: questão do GCC: usar um membro de uma class base que depende de um argumento de modelo . A resposta não é específica para o gcc.


Atualização: Em resposta ao comentário de mmichael , do rascunho N3337 do C ++ 11 Standard:

14.6.2 Nomes dependentes [temp.dep]
[…]
3 Na definição de um modelo de class ou class, se uma class base depender de um parâmetro de modelo, o escopo da class base não será examinado durante a consulta de nome não qualificado no ponto de definição do modelo ou membro de class ou durante uma instanciação de o modelo de class ou membro.

Se “porque o padrão diz isso” conta como uma resposta, não sei. Podemos agora perguntar por que o padrão exige que, como a excelente resposta de Steve Jessop e outros apontam, a resposta a esta última pergunta é bastante longa e discutível. Infelizmente, quando se trata do C ++ Standard, é quase impossível dar uma explicação curta e independente sobre o motivo pelo qual o padrão exige algo; isso se aplica também a esta última questão.