Resposta curta: para criar 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 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 foo
nã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. A
aqui 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::A
seria 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 T
em 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 typename
para 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 Foo
for 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::string
seria 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 Bar
seja definido antes Foo
e x
não seja um membro nessa definição, alguém poderá posteriormente definir uma especialização Bar
para 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 Bar
tivesse x
, eles poderiam definir uma especialização sem ela, e seu modelo procuraria um global x
para 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 A
dependentes:
using Bar<T>::A;
na classe - A
agora 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 typename
nã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::string
pode 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.