A tradução do artigo foi preparada especialmente para os alunos do curso "Desenvolvedor C ++" . É interessante desenvolver nessa direção? Assista à gravação da aula prática do Google Test Framework !

Parte 3 - Herança Virtual
Na primeira e na segunda partes deste artigo, falamos sobre como as vtables funcionam nos casos mais simples e depois na herança múltipla. A herança virtual complica ainda mais a situação.
Como você deve se lembrar, herança virtual significa que em uma classe específica há apenas uma instância da classe base. Por exemplo:
class ios ... class istream : virtual public ios ... class ostream : virtual public ios ... class iostream : public istream, public ostream
Se não fosse a palavra-chave virtual
acima, o iostream
duas instâncias do ios
que poderiam causar dores de cabeça durante a sincronização e seriam simplesmente ineficazes.
Para entender a herança virtual, consideraremos o seguinte fragmento de código:
#include <iostream> using namespace std; class Grandparent { public: virtual void grandparent_foo() {} int grandparent_data; }; class Parent1 : virtual public Grandparent { public: virtual void parent1_foo() {} int parent1_data; }; class Parent2 : virtual public Grandparent { public: virtual void parent2_foo() {} int parent2_data; }; class Child : public Parent1, public Parent2 { public: virtual void child_foo() {} int child_data; }; int main() { Child child; }
Vamos explorar child
. Começarei despejando uma grande quantidade de memória exatamente onde o vtable Child
começa, como fizemos nas partes anteriores, e analisarei os resultados. Sugiro dar uma rápida olhada no resultado aqui e retornar a ele quando divulgar os detalhes abaixo.
(gdb) p child $1 = {<Parent1> = {<Grandparent> = {_vptr$Grandparent = 0x400998 <vtable for Child+96>, grandparent_data = 0}, _vptr$Parent1 = 0x400950 <vtable for Child+24>, parent1_data = 0}, <Parent2> = {_vptr$Parent2 = 0x400978 <vtable for Child+64>, parent2_data = 4195888}, child_data = 0} (gdb) x/600xb 0x400938 0x400938 <vtable for Child>: 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400940 <vtable for Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400948 <vtable for Child+16>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400950 <vtable for Child+24>: 0x70 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400958 <vtable for Child+32>: 0xa0 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400960 <vtable for Child+40>: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400968 <vtable for Child+48>: 0xf0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400970 <vtable for Child+56>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400978 <vtable for Child+64>: 0x90 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400980 <vtable for Child+72>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400988 <vtable for Child+80>: 0xe0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400990 <vtable for Child+88>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400998 <vtable for Child+96>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x4009a0 <VTT for Child>: 0x50 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009a8 <VTT for Child+8>: 0xf8 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009b0 <VTT for Child+16>: 0x18 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009b8 <VTT for Child+24>: 0x98 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009c0 <VTT for Child+32>: 0xb8 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009c8 <VTT for Child+40>: 0x98 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009d0 <VTT for Child+48>: 0x78 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009d8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009e0 <construction vtable for Parent1-in-Child>: 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009e8 <construction vtable for Parent1-in-Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009f0 <construction vtable for Parent1-in-Child+16>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009f8 <construction vtable for Parent1-in-Child+24>: 0x70 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400a00 <construction vtable for Parent1-in-Child+32>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a08 <construction vtable for Parent1-in-Child+40>: 0xe0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400a10 <construction vtable for Parent1-in-Child+48>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a18 <construction vtable for Parent1-in-Child+56>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400a20 <typeinfo name for Parent1>: 0x37 0x50 0x61 0x72 0x65 0x6e 0x74 0x31 0x400a28 <typeinfo name for Parent1+8>: 0x00 0x31 0x31 0x47 0x72 0x61 0x6e 0x64 0x400a30 <typeinfo name for Grandparent+7>: 0x70 0x61 0x72 0x65 0x6e 0x74 0x00 0x00 0x400a38 <typeinfo for Grandparent>: 0x50 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400a40 <typeinfo for Grandparent+8>: 0x29 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a48: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a50 <typeinfo for Parent1>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400a58 <typeinfo for Parent1+8>: 0x20 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a60 <typeinfo for Parent1+16>: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x400a68 <typeinfo for Parent1+24>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a70 <typeinfo for Parent1+32>: 0x03 0xe8 0xff 0xff 0xff 0xff 0xff 0xff 0x400a78: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a80 <construction vtable for Parent2-in-Child>: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a88 <construction vtable for Parent2-in-Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a90 <construction vtable for Parent2-in-Child+16>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a98 <construction vtable for Parent2-in-Child+24>: 0x90 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400aa0 <construction vtable for Parent2-in-Child+32>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400aa8 <construction vtable for Parent2-in-Child+40>: 0xf0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400ab0 <construction vtable for Parent2-in-Child+48>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400ab8 <construction vtable for Parent2-in-Child+56>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400ac0 <typeinfo name for Parent2>: 0x37 0x50 0x61 0x72 0x65 0x6e 0x74 0x32 0x400ac8 <typeinfo name for Parent2+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400ad0 <typeinfo for Parent2>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400ad8 <typeinfo for Parent2+8>: 0xc0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400ae0 <typeinfo for Parent2+16>: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x400ae8 <typeinfo for Parent2+24>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400af0 <typeinfo for Parent2+32>: 0x03 0xe8 0xff 0xff 0xff 0xff 0xff 0xff 0x400af8 <typeinfo name for Child>: 0x35 0x43 0x68 0x69 0x6c 0x64 0x00 0x00 0x400b00 <typeinfo for Child>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400b08 <typeinfo for Child+8>: 0xf8 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b10 <typeinfo for Child+16>: 0x02 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x400b18 <typeinfo for Child+24>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b20 <typeinfo for Child+32>: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b28 <typeinfo for Child+40>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b30 <typeinfo for Child+48>: 0x02 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x400b38 <vtable for Grandparent>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b40 <vtable for Grandparent+8>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b48 <vtable for Grandparent+16>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00
Uau, há muita informação. Duas novas perguntas surgem imediatamente: o que é o VTT
e o que é a vtable for X-in-Child
construção vtable for X-in-Child
? Responderemos em breve.
Vamos começar com a estrutura de memória filho:
De fato, Child
tem apenas 1 instância de Grandparent. O que não é trivial é que ele é o último na memória, embora seja o mais alto da hierarquia.
Aqui está a estrutura vtable
:
Acima, há um novo conceito - virtual-base offset
. Em breve entenderemos o que ele está fazendo lá.
A seguir, vamos explorar essas construction vtables
aparência estranha. Aqui está a vtable for Parent1-in-Child
construção vtable for Parent1-in-Child
:
No momento, acho que seria mais compreensível descrever o processo do que empilhar mais tabelas com números aleatórios em você. Então:
Imagine que você é uma Child
. Você é solicitado a construir-se em um novo pedaço de memória. Como você herda Grandparent
diretamente (que é o que significa herança virtual), primeiro você chamará diretamente seu construtor (se não fosse herança virtual, você chamaria o construtor Parent1
, que por sua vez chamaria o construtor Grandparent
). Você define this += 32
bytes, pois é onde os dados do Grandparent
estão localizados e você chama o construtor. Muito simples
Então é hora de criar Parent1
. Parent1
pode assumir com segurança que, quando se constrói, o Grandparent
já foi criado, para que, por exemplo, obtenha acesso aos dados e métodos do Grandparent
. Mas espere, como ele pode saber onde encontrar esses dados? Eles não estão no mesmo lugar com as variáveis Parent1
!
A construction table for Parent1-in-Child
entra em cena. Esta tabela é para informar ao Parent1
onde encontrar os dados que ele pode acessar. this
aponta para os dados de Parent1
. virtual-base offset
indica onde você pode encontrar os dados do avô: Etapa 32 bytes a partir disso e você encontrará a memória do Grandparent
. Você entendeu? O deslocamento da base virtual é semelhante ao top_offset, mas para as classes virtuais.
Agora que entendemos isso, a construção do Parent2
é basicamente a mesma, usando apenas a construction table for Parent2-in-Child
. De fato, Parent2-in-Child
tem um virtual-base offset
de virtual-base offset
de 16 bytes.
Deixe a informação absorver um pouco. Você está pronto para continuar? Bom
Agora vamos voltar ao VTT
. Aqui está a estrutura da VTT
:
VTT
significa virtual-table table
, o que significa que é uma virtual-table table
v. Esta é uma tabela de conversão que sabe, por exemplo, se o construtor Parent1
para um único objeto, para Parent1-in-Child
ou Parent1-in-SomeOtherObject
. Ele sempre aparece imediatamente após a vtable
, para que o compilador saiba onde encontrá-lo. Portanto, não há necessidade de armazenar outro ponteiro nos próprios objetos.
Ah ... muitos detalhes, mas acho que cobrimos tudo o que eu queria cobrir. Na quarta parte, falaremos sobre os detalhes das vtables
nível superior. Não pule, pois esta é provavelmente a parte mais importante deste artigo!
Parte 4 - Código Gerado pelo Compilador
Neste ponto deste artigo, aprendemos como as typeinfo
e typeinfo
encaixam em nossos binários e como o compilador as utiliza. Agora vamos entender a parte do trabalho que o compilador faz por nós automaticamente.
Construtores
Para o construtor de qualquer classe, o seguinte código é gerado:
- Chamar construções pai, se houver;
- Definir ponteiros vtable, se houver;
- Inicialização de membros de acordo com a lista de inicializadores;
- Execução de código dentro dos colchetes do construtor.
Tudo o que foi dito acima pode acontecer sem um código explícito:
- Os construtores pai iniciam automaticamente por padrão, a menos que especificado de outra forma;
- Os membros são inicializados por padrão se não tiverem um valor padrão ou entradas na lista de inicializadores;
- O construtor inteiro pode ser marcado como padrão;
- Somente a atribuição de tabela está sempre oculta.
Aqui está um exemplo:
#include <iostream> #include <string> using namespace std; class Parent { public: Parent() { Foo(); } virtual ~Parent() = default; virtual void Foo() { cout << "Parent" << endl; } int i = 0; }; class Child : public Parent { public: Child() : j(1) { Foo(); } void Foo() override { cout << "Child" << endl; } int j; }; class Grandchild : public Child { public: Grandchild() { Foo(); s = "hello"; } void Foo() override { cout << "Grandchild" << endl; } string s; }; int main() { Grandchild g; }
Vamos escrever um pseudo-código para o construtor de cada classe:
Dado isso, não é de surpreender que, no contexto do construtor da classe, vtable se refira à vtable dessa classe e não à sua classe específica. Isso significa que as chamadas virtuais são resolvidas como se não houvesse herdeiros disponíveis. Assim, a conclusão
Parent Child Grandchild
E as funções virtuais puras? Se elas não forem implementadas (sim, você pode implementar funções puramente virtuais, mas por que você precisa disso?), Você provavelmente (e espero) irá direto para o segfault. Alguns compiladores negligenciam o erro, o que é interessante.
Destrutores
Como você pode imaginar, os destruidores se comportam da mesma maneira que os construtores, apenas na ordem inversa.
Aqui está um rápido exercício para pensar: por que os destruidores alteram o ponteiro do vtable para que ele aponte para sua própria classe, em vez de deixar um ponteiro para uma classe específica? Resposta: desde que o destruidor foi lançado, qualquer classe herdada já estava destruída. Chamar métodos dessa classe não é o que você deseja fazer.
Elenco implícito
Como vimos na segunda e terceira partes , um ponteiro para um objeto filho não é necessariamente igual ao ponteiro pai da mesma instância (como no caso de herança múltipla).
No entanto, para você (o desenvolvedor), não há trabalho adicional para chamar uma função que recebe um ponteiro pai. Isso ocorre porque o compilador muda implicitamente this
quando você anexa ponteiros e referências às classes pai.
Transmissão dinâmica (RTTI)
As conversões dinâmicas usam tabelas typeinfo
, que examinamos na primeira parte. Eles fazem isso em tempo de execução, observando a entrada typeinfo
um ponteiro antes do que o ponteiro vtable
aponta e usam a classe a partir daí para verificar se o elenco é possível.
Isso explica o custo do dynamic_cast quando usado com freqüência.
Ponteiros de método
Pretendo escrever um post completo sobre indicadores de métodos no futuro. Antes disso, eu gostaria de enfatizar que um ponteiro para um método que aponta para uma função virtual realmente chama um método substituído (em oposição a ponteiros para funções que não são membros).
// TODO: ,
Verifique você mesmo!
Agora você pode explicar por que o seguinte fragmento de código se comporta da maneira que se comporta:
#include <iostream> using namespace std; class FooInterface { public: virtual ~FooInterface() = default; virtual void Foo() = 0; }; class BarInterface { public: virtual ~BarInterface() = default; virtual void Bar() = 0; }; class Concrete : public FooInterface, public BarInterface { public: void Foo() override { cout << "Foo()" << endl; } void Bar() override { cout << "Bar()" << endl; } }; int main() { Concrete c; c.Foo(); c.Bar(); FooInterface* foo = &c; foo->Foo(); BarInterface* bar = (BarInterface*)(foo); bar->Bar(); // "Foo()" - WTF? }
Isso conclui meu artigo de quatro partes. Espero que você aprenda algo novo, assim como eu.