Resposta curta: para criar xum 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 compilação em duas fases e o MSVC não faz isso, mas é exigido pelo padrão e implementado pelos outros principais compiladores. Se desejar, o compilador deve compilar o modelo assim que o visualizar (para algum tipo de representação interna da árvore de análise) e adiar a compilação da instanciação para mais tarde.
As verificações executadas no próprio modelo, e não em instâncias específicas, 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 é do tipo ou não. Por exemplo:
#if WANT_POINTER
typedef int A;
#else
int A;
#endif
static const int x = 2;
template <typename T> void foo() { A *x = 0; }
se A é um tipo, que declara um ponteiro (com nenhum efeito além de sombrear o global x). Se A é um objeto, isso é multiplicação (e barrar algum operador sobrecarregar é ilegal, atribuir a um rvalor). Se estiver errado, esse erro deve ser diagnosticado na fase 1 , é definido pelo padrão como um erro no modelo , não em uma instanciação específica dele. Mesmo que o modelo nunca seja instanciado, se A é um int, o código acima está mal formado e deve ser diagnosticado, como seria se foonão fosse um modelo, mas uma função simples.
Agora, o padrão diz que nomes que não são dependentes dos parâmetros do modelo devem ser resolvidos na fase 1. Aaqui não é um nome dependente, ele se refere à mesma coisa, independentemente do tipo T. Portanto, ele precisa ser definido antes que o modelo seja definido para ser encontrado e verificado na fase 1.
T::Aseria um nome que depende de T. Não podemos saber na fase 1 se esse é um tipo ou não. O tipo que eventualmente será usado como Tem uma instanciação provavelmente ainda não está definido e, mesmo que não seja, não sabemos que tipo (s) será usado como nosso parâmetro de modelo. Mas temos que resolver a gramática para fazer nossas preciosas verificações de fase 1 quanto a modelos mal formados. Portanto, o padrão possui uma regra para nomes dependentes - o compilador deve assumir que eles não são do tipo, a menos que sejam qualificados typenamepara especificar que são do tipo ou usados em determinados contextos inequívocos. Por exemplo template <typename T> struct Foo : T::A {};, em , T::Aé usado como uma classe base e, portanto, é um tipo sem ambiguidade. Se Foofor instanciado com algum tipo que tenha um membro de dadosA em vez de um tipo aninhado A, isso é um erro no código que executa a instanciação (fase 2), não um erro no modelo (fase 1).
Mas e um modelo de classe com uma classe base dependente?
template <typename T>
struct Foo : Bar<T> {
Foo() { A *x = 0; }
};
A é um nome dependente ou não? Com as classes base, qualquer nome pode aparecer na classe base. Então, 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 no Foo é dependente e, portanto, todos os tipos usados no Foo (exceto os tipos incorporados) ser qualificados. Dentro do Foo, você teria que escrever:
typename std::string s = "hello, world";
porque std::stringseria um nome dependente e, portanto, assumido como não-tipo, a menos que especificado de outra forma. Ai!
Um segundo problema com a permissão do seu código preferido ( return x;) é que, mesmo que Barseja definido antes Fooe xnão seja um membro nessa definição, alguém poderá posteriormente definir uma especialização Barpara algum tipo Baz, como Bar<Baz>um membro de dados x, e instanciar Foo<Baz>. Portanto, nessa instanciação, seu modelo retornaria o membro de dados em vez de retornar o global x. Ou, inversamente, se a definição do modelo base Bartivesse x, eles poderiam definir uma especialização sem ela, e seu modelo procuraria um global xpara retornar Foo<Baz>. Eu 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 em vigor diz que classes base dependentes de modelos de classe simplesmente não são consideradas para pesquisa, a menos que seja explicitamente solicitado. 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ê precisa qualificar as coisas da classe base ou elas não foram encontradas. Existem três maneiras comuns de tornar Adependentes:
using Bar<T>::A;na classe - Aagora se refere a algo dentro Bar<T>, portanto dependente.
Bar<T>::A *x = 0;no ponto de uso - Mais uma vez, Aé definitivamente no Bar<T>. Isso é multiplicação, uma vez que typenamenão foi usado, possivelmente um mau exemplo, mas teremos que esperar até a instanciação para descobrir se operator*(Bar<T>::A, x)retorna um rvalor. Quem sabe, talvez sim ...
this->A;no ponto de uso - Aé um membro, por isso, se não estiver emFoo estiver, deve estar na classe base, novamente o padrão diz que isso o torna dependente.
A compilação em duas fases é complicada e difícil e apresenta alguns requisitos surpreendentes para palavreado extra no seu código. Mas, assim como a democracia, é provavelmente a pior maneira possível de fazer as coisas, além de todas as outras.
Você poderia argumentar razoavelmente que, no seu exemplo, return x;não faz sentido se xé um tipo aninhado na classe 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 semthis-> . Até certo ponto, você é vítima de danos colaterais da solução para um problema que não se aplica ao seu caso, mas ainda há o problema de sua classe base potencialmente introduzir nomes sob você que sombream globais ou não ter nomes que você pensou eles tinham, e um global sendo encontrado.
Você também pode argumentar que o padrão deve ser o oposto dos nomes dependentes (assuma o tipo, a menos que seja especificado de algum modo como um objeto), ou que o padrão seja mais sensível ao contexto (em std::string s = "";, std::stringpode ser lido como um tipo, pois nada mais torna gramatical sentido, mesmo sendo std::string *s = 0;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 mitigou a criação de muitas regras específicas para quais contextos assumem um tipo e quais não são.