Por que tenho que acessar os membros da classe base do modelo por meio desse ponteiro?

199

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

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;
}
Todos
fonte
1
Ah caramba. Tem algo a ver com pesquisa de nome. Se alguém não responder isso em breve, procurarei e publicarei (ocupado agora).
templatetypedef
@ Ed Swangren: Desculpe, eu perdi entre as respostas oferecidas ao postar esta pergunta. Eu estava procurando a resposta há muito tempo antes disso.
Ali
6
Isso acontece devido à pesquisa de nome em duas fases (que nem todos os compiladores usam por padrão) e aos nomes dependentes. Existem 3 soluções para esse problema, além de prefixar ox with this->, a saber: 1) Use o prefixo base<T>::x, 2) Adicione uma instrução using base<T>::x, 3) Use uma opção de compilador global que habilite o modo permissivo. Os prós e contras dessas soluções são descritos em stackoverflow.com/questions/50321788/...
George Robinson

Respostas:

274

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.

Steve Jessop
fonte
1
Ooh, boa resposta detalhada. Esclareceu algumas coisas que nunca me preocupei em procurar. :) +1
jalf
20
@ jalf: existe algo como o C ++ QTWBFAETYNSYEWTKTAAHMITTBGOW - "Perguntas que seriam feitas com freqüência, exceto que você não tem certeza de que deseja saber a resposta e tem coisas mais importantes para se dar bem"?
Steve Jessop
4
resposta extraordinária, pergunto-me se a pergunta poderia caber no faq.
Matthieu M.
Whoa, podemos dizer enciclopédicos? highfive Um ponto sutil: "Se Foo é instanciado com algum tipo que possui um membro de dados A em vez de um tipo aninhado A, isso é um erro no código que faz a instanciação (fase 2), não um erro no modelo (fase 1) " Pode ser melhor dizer que o modelo não está malformado, mas isso ainda pode ser um caso de suposição incorreta ou bug lógico por parte do criador do modelo. Se a instanciação sinalizada fosse realmente o caso de uso pretendido, o modelo estaria errado.
Ionoclast Brigham
1
@JohnH. Dado que vários compiladores implementam -fpermissiveou similares, sim, é possível. Não sei os detalhes de como ele foi implementado, mas o compilador deve estar adiando a resolução xaté conhecer a classe base real do tempate T. Portanto, em princípio, no modo não permissivo, ele pode registrar o fato de que foi adiado, adiado, fazer a pesquisa assim que for realizada Te, quando a pesquisa for bem-sucedida, emita o texto que você sugere. Seria uma sugestão muito precisa, se isso fosse feito apenas nos casos em que funciona: as chances de o usuário ter significado outro xde outro escopo são bem pequenas!
Steve Jessop
13

(Resposta original de 10 de janeiro de 2011)

Acho que encontrei a resposta: Problema no GCC: usando um membro de uma classe 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 da norma C ++ 11:

14.6.2 Nomes dependentes [temp.dep]
[...]
3 Na definição de uma classe ou modelo de classe, se uma classe base depende de um parâmetro de modelo, o escopo da classe base não é examinado durante a pesquisa de nome não qualificado em o ponto de definição do modelo ou membro da classe ou durante uma instanciação do modelo ou membro da classe.

Se "porque o padrão diz isso" conta como resposta, eu não sei. Agora podemos perguntar por que o padrão exige isso, mas, 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 padrão C ++, muitas vezes é quase impossível fornecer uma explicação curta e independente de por que o padrão exige algo; isso se aplica também à última questão.

Todos
fonte
11

O xestá oculto durante a herança. Você pode reexibir via:

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

public:
    using base<T>::x;             // added "using" statement
    int f() { return x; }
};
chrisaycock
fonte
25
Esta resposta não explica por que está oculta.
jamesdlin