O que é um vtable
?
Pode ser útil saber do que a mensagem de erro está falando antes de tentar corrigi-la. Vou começar em um nível alto e depois trabalhar com mais alguns detalhes. Dessa forma, as pessoas podem pular adiante assim que se sentirem confortáveis com o entendimento das tabelas vt. ... e lá vai um monte de pessoas pulando adiante agora. :) Para aqueles que ficam por aqui:
Uma vtable é basicamente a implementação mais comum de polimorfismo em C ++ . Quando vtables são usadas, toda classe polimórfica tem uma tabela em algum lugar do programa; você pode pensar nisso como um static
membro de dados (oculto) da classe. Todo objeto de uma classe polimórfica é associado ao vtable para sua classe mais derivada. Ao verificar essa associação, o programa pode trabalhar sua mágica polimórfica. Advertência importante: uma vtable é um detalhe de implementação. Não é obrigatório pelo padrão C ++, embora a maioria dos compiladores C ++ (todos?) Use vtables para implementar comportamento polimórfico. Os detalhes que estou apresentando são abordagens típicas ou razoáveis. Compiladores podem se desviar disso!
Cada objeto polimórfico possui um ponteiro (oculto) para a vtable para a classe mais derivada do objeto (possivelmente vários ponteiros, nos casos mais complexos). Observando o ponteiro, o programa pode dizer qual é o tipo "real" de um objeto (exceto durante a construção, mas vamos pular esse caso especial). Por exemplo, se um objeto do tipo A
não apontar para a vtable de A
, então esse objeto é realmente um subobjeto de algo derivado A
.
O nome "vtable" vem de " v irtual function table ". É uma tabela que armazena ponteiros para funções (virtuais). Um compilador escolhe sua convenção de como a tabela é organizada; Uma abordagem simples é analisar as funções virtuais na ordem em que são declaradas nas definições de classe. Quando uma função virtual é chamada, o programa segue o ponteiro do objeto até uma tabela, vai para a entrada associada à função desejada e usa o ponteiro da função armazenada para chamar a função correta. Existem vários truques para fazer isso funcionar, mas não vou abordar esses aqui.
Onde / quando é vtable
gerado?
Uma vtable é gerada automaticamente (às vezes chamada de "emitida") pelo compilador. Um compilador pode emitir uma vtable em todas as unidades de tradução que veem uma definição de classe polimórfica, mas isso geralmente seria um exagero desnecessário. Uma alternativa ( usada pelo gcc e provavelmente por outros) é escolher uma única unidade de tradução na qual colocar a vtable, semelhante a como você escolheria um único arquivo de origem para colocar os membros de dados estáticos de uma classe. Se esse processo de seleção falhar na escolha de quaisquer unidades de tradução, a tabela v se torna uma referência indefinida. Daí o erro, cuja mensagem não é reconhecidamente clara.
Da mesma forma, se o processo de seleção escolher uma unidade de tradução, mas esse arquivo de objeto não for fornecido ao vinculador, a vtable se tornará uma referência indefinida. Infelizmente, a mensagem de erro pode ser ainda menos clara nesse caso do que no caso em que o processo de seleção falhou. (Obrigado aos respondentes que mencionaram essa possibilidade. Eu provavelmente teria esquecido o contrário.)
O processo de seleção usado pelo gcc faz sentido se começarmos com a tradição de dedicar um arquivo de origem (único) a cada classe que precisa de um para sua implementação. Seria bom emitir a vtable ao compilar esse arquivo de origem. Vamos chamar isso de nosso objetivo. No entanto, o processo de seleção precisa funcionar, mesmo que essa tradição não seja seguida. Portanto, em vez de procurar a implementação de toda a classe, vamos procurar a implementação de um membro específico da classe. Se a tradição for seguida - e se esse membro for de fato implementado -, isso alcançará o objetivo.
O membro selecionado pelo gcc (e potencialmente por outros compiladores) é a primeira função virtual não embutida que não é virtual pura. Se você faz parte da multidão que declara construtores e destruidores antes de outras funções-membro, esse destruidor tem uma boa chance de ser selecionado. (Você se lembrou de tornar o destruidor virtual, certo?) Há exceções; Eu esperaria que as exceções mais comuns sejam quando uma definição em linha é fornecida para o destruidor e quando o destruidor padrão é solicitado (usando " = default
").
O astuto pode perceber que uma classe polimórfica pode fornecer definições em linha para todas as suas funções virtuais. Isso não causa falha no processo de seleção? Isso acontece em compiladores mais antigos. Eu li que os compiladores mais recentes abordaram essa situação, mas não sei os números de versão relevantes. Eu poderia tentar procurar isso, mas é mais fácil codificá-lo ou aguardar a reclamação do compilador.
Em resumo, há três causas principais do erro "referência indefinida à vtable":
- Uma função de membro está ausente em sua definição.
- Um arquivo de objeto não está sendo vinculado.
- Todas as funções virtuais têm definições embutidas.
Essas causas são, por si só, insuficientes para causar o erro por conta própria. Em vez disso, é isso que você abordaria para resolver o erro. Não espere que a criação intencional de uma dessas situações definitivamente produza esse erro; existem outros requisitos. Espere que a solução dessas situações resolva esse erro.
(OK, o número 3 pode ter sido suficiente quando essa pergunta foi feita.)
Como corrigir o erro?
Bem-vindo de volta, pessoas pulando à frente! :)
- Veja a sua definição de classe. Encontre a primeira função virtual não embutida que não é virtual pura (não "
= 0
") e cuja definição você fornece (não " = default
").
- Se não houver essa função, tente modificar sua classe para que exista uma. (Erro possivelmente resolvido.)
- Veja também a resposta de Philip Thomas para uma advertência.
- Encontre a definição para essa função. Se estiver faltando, adicione-o! (Erro possivelmente resolvido.)
- Verifique seu comando de link. Se ele não mencionar o arquivo de objeto com a definição dessa função, corrija isso! (Erro possivelmente resolvido.)
- Repita as etapas 2 e 3 para cada função virtual e, em seguida, para cada função não virtual, até que o erro seja resolvido. Se você ainda estiver travado, repita para cada membro de dados estático.
Exemplo
Os detalhes do que fazer podem variar e às vezes se ramificam em perguntas separadas (como O que é uma referência indefinida / erro de símbolo externo não resolvido e como faço para corrigi-lo? ). Entretanto, fornecerei um exemplo do que fazer em um caso específico que pode atrapalhar os programadores mais novos.
A Etapa 1 menciona a modificação de sua classe para que ela tenha uma função de um determinado tipo. Se a descrição dessa função passou por sua cabeça, você pode estar na situação que pretendo abordar. Lembre-se de que essa é uma maneira de atingir a meta; não é o único caminho, e facilmente poderia haver caminhos melhores em sua situação específica. Vamos ligar para a sua turma A
. Seu destruidor é declarado (na sua definição de classe) como
virtual ~A() = default;
ou
virtual ~A() {}
? Nesse caso, duas etapas mudarão seu destruidor para o tipo de função que queremos. Primeiro, mude essa linha para
virtual ~A();
Segundo, coloque a seguinte linha em um arquivo de origem que faça parte do seu projeto (de preferência o arquivo com a implementação da classe, se você tiver um):
A::~A() {}
Isso faz com que seu destruidor (virtual) não esteja em linha e não seja gerado pelo compilador. (Sinta-se à vontade para modificar as coisas para corresponder melhor ao seu estilo de formatação de código, como adicionar um comentário de cabeçalho à definição da função.)