O que é uma mesa virtual?

Uma vez no Slack, me deparei com um novo acrônimo para o meu glossário de acrônimos em C ++ : "VTT". Godbolt :

test.o: In function `MyClass': test.cc:3: undefined reference to `VTT for MyClass' 

"VTT" neste contexto significa "tabela de tabela virtual". Essa é uma estrutura de dados auxiliar usada (no Itanium C ++ ABI) ao criar algumas classes de base que são herdadas das classes de base virtuais. As VTTs seguem as mesmas regras de layout que as tabelas virtuais (vtable) e as informações de tipo (typeinfo); portanto, se você receber o erro acima, poderá substituir mentalmente “vtable” por “VTT” e iniciar a depuração. (Muito provavelmente, você deixou a função principal da classe indefinida). Para ver por que a VTT, ou uma estrutura semelhante, é necessária, vamos começar com o básico.

Ordem de design para herança não virtual


Quando temos uma hierarquia de herança, as classes base são construídas a partir das mais básicas . Para construir Charlie, primeiro precisamos construir as classes pai MrsBucket e MrBucket, recursivamente, para construir MrBucket, primeiro devemos construir as classes pai GrandmaJosephine e GrandpaJoe.

Assim:

 struct A {}; struct B : A {}; struct C {}; struct D : C {}; struct E : B, D {}; //     // ABCDE 

Ordem de design para classes base virtuais


Mas a herança virtual confunde todos os cartões! Com a herança virtual, podemos ter uma hierarquia em forma de diamante na qual duas classes principais diferentes podem compartilhar um ancestral comum.

 struct G {}; struct M : virtual G {}; struct F : virtual G {}; struct E : M, F {}; //     // GMFE 

Na última seção, cada construtor foi responsável por chamar o construtor de sua classe base. Mas agora temos herança virtual, e os construtores M e F devem, de alguma forma, saber que não é necessário construir G, porque é comum. Se M e F fossem responsáveis ​​pela construção dos objetos base nesse caso, o objeto base comum seria construído duas vezes, o que não é muito bom.

Para trabalhar com subobjetos de herança virtual, o Itanium C ++ ABI divide cada construtor em duas partes: o construtor de objeto base e o construtor de objeto completo. O construtor do objeto base é responsável por construir todos os subobjetos de herança não virtual (e seus subobjetos, e instalar seu vptr em sua tabela v, e executar o código entre chaves no código C ++). O construtor do objeto completo, chamado toda vez que você cria o objeto C ++ completo, é responsável por construir todos os subobjetos da herança virtual do objeto derivado e, em seguida, faz o resto.

Considere a diferença entre o nosso exemplo ABCDE da seção anterior e o seguinte exemplo:

 struct A {}; struct B : virtual A {}; struct C {}; struct D : virtual C {}; struct E : B, D {}; //     // ACBDE 

O construtor do objeto completo E primeiro chama os construtores do objeto base dos subobjetos virtuais A e C; então, os construtores do objeto de herança não virtual de base B e D. são chamados. B e D não são mais responsáveis ​​pela construção de A e C, respectivamente.

Projetando tabelas vtable


Suponha que tenhamos uma classe com alguns métodos virtuais, por exemplo, como ( Godbolt ):

 struct Cat { Cat() { poke(); } virtual void poke() { puts("meow"); } }; struct Lion : Cat { std::string roar = "roar"; Lion() { poke(); } void poke() override { roar += '!'; puts(roar.c_str()); } }; 

Quando construímos o Lion, começamos construindo o subobjeto básico do gato. O construtor de Cat chama poke (). Neste ponto, temos apenas um objeto Cat - ainda não inicializamos os dados do membro necessários para criar o objeto Lion. Se o construtor Cat chamar Lion :: poke (), ele poderá tentar alterar o membro não inicializado de std :: string roar e obteremos UB. Portanto, o padrão C ++ nos obriga a fazer isso no construtor Cat, uma chamada ao método virtual poke () deve chamar Cat :: poke (), não Lion :: poke ()!

Não tem problema. O compilador simplesmente faz com que Cat :: Cat () (a versão para o objeto base e a versão para o objeto completo) inicie definindo o vptr do objeto para a vtable do objeto Cat. Lion :: Lion () chamará Cat :: Cat () e, em seguida, redefinirá vptr para o ponteiro da tabela de objetos Cat dentro de Lion, antes de executar o código entre parênteses. Não tem problema!

Compensações de herança virtual


Deixe o gato virtualmente herdar do Animal. Em seguida, a vtable para Cat armazena não apenas os ponteiros de função para as funções membro virtuais do Cat, mas também o deslocamento do subobjeto virtual Animal no Cat. ( Godbolt .)

 struct Animal { const char *data = "hi"; }; struct Cat : virtual Animal { Cat() { puts(data); } }; struct Nermal : Cat {}; struct Garfield : Cat { int padding; }; 

O construtor Cat consulta o membro Animal :: data. Se esse objeto Cat for o subobjeto base do objeto Nermal, seus dados de membro estarão no deslocamento 8, logo atrás do vptr. Mas se o objeto Cat for o subobjeto subjacente do objeto Garfield, os dados do membro estarão no deslocamento 16, atrás de vptr e Garfield :: padding. Para lidar com isso, o Itanium ABI armazena os deslocamentos dos objetos de base virtuais na tabela do objeto Cat. A tabela para Cat-in-Nermal preserva o fato de que Animal, o subobjeto de gato subjacente, é armazenado no deslocamento 8; A tabela para Cat-in-Garfield mantém o fato de que Animal, o subobjeto de gato subjacente, é armazenado no deslocamento 16.

Agora combine isso com a seção anterior. O compilador deve garantir que Cat :: Cat () (a versão do objeto base e a versão do objeto completo) inicie a instalação do vptr no vtable para Cat-in-Nermal ou no vtable para Cat-in-Garfield, dependendo do tipo mais facilidade derivada! Mas como isso funciona?

O construtor do objeto completo para o objeto mais derivado deve pré-calcular qual tabela vtable ele deseja que o vptr do subobjeto base faça referência durante o tempo de construção do objeto e, em seguida, o construtor do objeto completo para o objeto mais derivado deve passar essas informações para o construtor do objeto base do subobjeto base como um parâmetro oculto! Vejamos o código gerado para Cat :: Cat () ( Godbolt ):

 _ZN3CatC1Ev: #    Cat movq $_ZTV3Cat+24, (%rdi) # this->vptr = &vtable-for-Cat; retq _ZN3CatC2Ev: #     Cat movq (%rsi), %rax # fetch a value from rsi movq %rax, (%rdi) # this->vptr = *rsi; retq 

O construtor do objeto base aceita não apenas esse parâmetro oculto em% rdi, mas também o parâmetro VTT oculto em% rsi! O construtor de objeto base carrega o endereço de (% rsi) e armazena o endereço na tabela de tabelas do objeto Cat.

Quem chama o construtor do objeto Cat base é responsável por prever qual endereço Cat :: Cat () deve ser escrito no vptr e por definir o ponteiro em (% rsi) para esse endereço.

Por que precisamos de outro nível de identidade?


Considere o construtor de um objeto Nermal completo.

 _ZN3CatC2Ev: #    Cat movq (%rsi), %rax #    rsi movq %rax, (%rdi) # this->vptr = *rsi; retq _ZN6NermalC1Ev: #    Nermal pushq %rbx movq %rdi, %rbx movl $_ZTT6Nermal+8, %esi # %rsi = &VTT-for-Nermal callq _ZN3CatC2Ev #     Cat movq $_ZTV6Nermal+24, (%rbx) # this->vptr = &vtable-for-Nermal popq %rbx retq _ZTT6Nermal: .quad _ZTV6Nermal+24 # vtable-for-Nermal .quad _ZTC6Nermal0_3Cat+24 # construction-vtable-for-Cat-in-Nermal 

Por que o _ZTC6Nermal0_3Cat + 24 está localizado na seção de dados e seu endereço é passado para% rsi, em vez de apenas passar diretamente o _ZTC6Nermal0_3Cat + 24?

 #   ? _ZN3CatC2Ev: #     Cat movq %rsi, (%rdi) # this->vptr = rsi; retq _ZN6NermalC1Ev: #     Nermal pushq %rbx movq %rdi, %rbx movl $_ZTC6Nermal0_3Cat+24, %esi # %rsi = &construction-vtable-for-Cat-in-Nermal callq _ZN3CatC2Ev #     Cat movq $_ZTV6Nermal+24, (%rbx) # this->vptr = &vtable-for-Nermal popq %rbx retq 

Isso ocorre porque podemos ter vários níveis de herança! Em cada nível de herança, o construtor do objeto base deve definir vptr e, em seguida, possivelmente, passar o controle da cadeia para o próximo construtor base, que pode definir vptrs para outro valor. Isso implica em uma lista ou uma tabela de ponteiros para o vtable.

Aqui está um exemplo concreto ( Godbolt ):

 struct VB { int member_of_vb = 42; }; struct Grandparent : virtual VB { Grandparent() {} }; struct Parent : Grandparent { Parent() {} }; struct Gretel : Parent { Gretel() : VB{1000} {} }; struct Hansel : Parent { int padding; Hansel() : VB{2000} {} }; 

O objeto construtor de base Grandparent deve definir seu vptr como Grandparent - outra coisa, que é a classe mais derivada. O construtor do objeto base Parent deve primeiro chamar Grandparent :: Grandparent () com o% rsi apropriado e, em seguida, definir vptr como Parent - outra coisa, que é a classe mais derivada. Uma maneira de implementar isso para Gretel:

 Gretel::Gretel() [  ]: pushq %rbx movq %rdi, %rbx movl $1000, 8(%rdi) # imm = 0x3E8 movl $VTT for Gretel+8, %esi callq Parent::Parent() [  ] movq $vtable for Gretel+24, (%rbx) popq %rbx retq VTT for Gretel: .quad vtable for Gretel+24 .quad construction vtable for Parent-in-Gretel+24 .quad construction vtable for Grandparent-in-Gretel+24 

Você pode ver no Godbolt que o construtor do objeto base da classe Parent primeiro chama Grandparent :: Grandparent () com% rsi + 8 e depois define seu próprio vptr como (% rsi). Então, aqui estamos usando o fato de que Gretel, por assim dizer, estabeleceu cuidadosamente um caminho de migalhas ao longo do qual todas as suas classes básicas seguiram durante a construção.

O mesmo VTT é usado no destruidor ( Godbolt ). Tanto quanto eu sei, a linha nula da tabela VTT nunca é usada. O construtor Gretel carrega a vtable para Gretel + 24 no vptr, mas ele sabe que esse endereço é estático, nunca precisa ser carregado pelo VTT. Eu acho que a linha zero da tabela foi preservada simplesmente por razões históricas. (E, claro, o compilador não pode simplesmente jogá-lo fora, porque será uma violação do Itanium ABI e será impossível vincular ao código antigo que adere ao Itanium-ABI).

Isso é tudo, vimos uma tabela de tabelas virtuais, ou VTT.

Maiores informações


Você pode encontrar informações da VTT nesses locais:

StackOverflow: “ O que é o VTT para uma classe? "
Notas da VTable sobre herança múltipla no GCC C ++ Compiler v4.0.1 ” (Morgan Deters, 2005)
A seção Itanium C ++ ABI , “Ordem VTT”

Finalmente, tenho que reiterar que a VTT é um recurso do Itanium C ++ ABI e é usado no Linux, OSX, etc. A ABV do MSVC usada no Windows não possui VTT e usa um mecanismo completamente diferente para herança virtual. Até agora, não sei quase nada sobre a MSVC ABI, mas talvez um dia eu descubra tudo e escreva um post sobre isso!

Source: https://habr.com/ru/post/pt474318/


All Articles