Tabela de método virtual - Virtual method table

Uma tabela virtual método ( VMT ), tabela virtual função , tabela de chamada virtual , tabela de despacho , TabelaV , ou vftable é um mecanismo usado em uma linguagem de programação de suporte expedição dinâmica (ou em tempo de execução método de ligação ).

Sempre que uma classe define uma função (ou método) virtual , a maioria dos compiladores adiciona uma variável de membro oculta à classe que aponta para um array de ponteiros para funções (virtuais) chamadas de tabela de método virtual. Esses ponteiros são usados ​​em tempo de execução para invocar as implementações de função apropriadas, porque em tempo de compilação pode ainda não ser conhecido se a função base deve ser chamada ou se uma função derivada deve ser implementada por uma classe que herda da classe base.

Existem muitas maneiras diferentes de implementar esse despacho dinâmico, mas o uso de tabelas de métodos virtuais é especialmente comum entre C ++ e linguagens relacionadas (como D e C # ). Linguagens que separam a interface programática de objetos da implementação, como Visual Basic e Delphi , também tendem a usar essa abordagem, porque permite que os objetos usem uma implementação diferente simplesmente usando um conjunto diferente de ponteiros de método.

Suponha que um programa contém três aulas em uma herança hierarquia: uma superclasse , Cate duas subclasses , HouseCate Lion. Class Catdefine uma função virtual chamada speak, portanto, suas subclasses podem fornecer uma implementação apropriada (por exemplo, meowou roar). Quando o programa chama a speakfunção em uma Catreferência (que pode se referir a uma instância de Cat, ou uma instância de HouseCatou Lion), o código deve ser capaz de determinar para qual implementação da função a chamada deve ser despachada . Isso depende da classe real do objeto, não da classe da referência a ele ( Cat). A classe geralmente não pode ser determinada estaticamente (ou seja, em tempo de compilação ), portanto, o compilador também não pode decidir qual função chamar naquele momento. A chamada deve ser despachada para a função correta dinamicamente (ou seja, em tempo de execução ).

Implementação

A tabela de método virtual de um objeto conterá os endereços dos métodos vinculados dinamicamente do objeto. Chamadas de método são realizadas buscando o endereço do método na tabela de método virtual do objeto. A tabela de método virtual é a mesma para todos os objetos pertencentes à mesma classe e, portanto, é normalmente compartilhada entre eles. Objetos pertencentes a classes compatíveis com o tipo (por exemplo, irmãos em uma hierarquia de herança) terão tabelas de métodos virtuais com o mesmo layout: o endereço de um determinado método aparecerá no mesmo deslocamento para todas as classes compatíveis com o tipo. Portanto, obter o endereço do método de um determinado deslocamento em uma tabela de método virtual obterá o método correspondente à classe real do objeto.

Os padrões C ++ não determinam exatamente como o despacho dinâmico deve ser implementado, mas os compiladores geralmente usam pequenas variações no mesmo modelo básico.

Normalmente, o compilador cria uma tabela de método virtual separada para cada classe. Quando um objeto é criado, um ponteiro para essa tabela, denominado ponteiro de tabela virtual , vpointer ou VPTR , é adicionado como um membro oculto desse objeto. Como tal, o compilador também deve gerar código "oculto" nos construtores de cada classe para inicializar o ponteiro da tabela virtual de um novo objeto para o endereço da tabela de método virtual de sua classe.

Muitos compiladores colocam o ponteiro da tabela virtual como o último membro do objeto; outros compiladores o colocam como o primeiro; o código-fonte portátil funciona de qualquer maneira. Por exemplo, g ++ colocou anteriormente o ponteiro no final do objeto.

Exemplo

Considere as seguintes declarações de classe na sintaxe C ++ :

class B1 {
public:
  virtual ~B1() {}
  void f0() {}
  virtual void f1() {}
  int int_in_b1;
};

class B2 {
public:
  virtual ~B2() {}
  virtual void f2() {}
  int int_in_b2;
};

usado para derivar a seguinte classe:

class D : public B1, public B2 {
public:
  void d() {}
  void f2() override {}
  int int_in_d;
};

e a seguinte parte do código C ++:

B2 *b2 = new B2();
D  *d  = new D();

O g ++ 3.4.6 do GCC produz o seguinte layout de memória de 32 bits para o objeto b2:

b2:
  +0: pointer to virtual method table of B2
  +4: value of int_in_b2

virtual method table of B2:
  +0: B2::f2()   

e o seguinte layout de memória para o objeto d:

d:
  +0: pointer to virtual method table of D (for B1)
  +4: value of int_in_b1
  +8: pointer to virtual method table of D (for B2)
 +12: value of int_in_b2
 +16: value of int_in_d

Total size: 20 Bytes.

virtual method table of D (for B1):
  +0: B1::f1()  // B1::f1() is not overridden

virtual method table of D (for B2):
  +0: D::f2()   // B2::f2() is overridden by D::f2()

Observe que essas funções que não contêm a palavra-chave virtualem sua declaração (como f0()e d()) geralmente não aparecem na tabela de método virtual. Existem exceções para casos especiais, conforme apresentado pelo construtor padrão .

Observe também os destruidores virtuais nas classes base B1e B2. Eles são necessários para garantir que delete dpossam liberar memória não apenas para D, mas também para B1e B2, se dfor um ponteiro ou referência para os tipos B1ou B2. Eles foram excluídos dos layouts de memória para manter o exemplo simples.

A substituição do método f2()na classe Dé implementada duplicando a tabela de método virtual de B2e substituindo o ponteiro para B2::f2()por um ponteiro para D::f2().

Herança múltipla e thunks

O compilador g ++ implementa a herança múltipla das classes B1e B2na classe Dusando duas tabelas de métodos virtuais, uma para cada classe base. (Existem outras maneiras de implementar herança múltipla, mas esta é a mais comum.) Isso leva à necessidade de "ajustes de ponteiro", também chamados de thunks , durante a conversão .

Considere o seguinte código C ++:

D  *d  = new D();
B1 *b1 = d;
B2 *b2 = d;

Enquanto de b1irá apontar para o mesmo local da memória após a execução deste código, b2irá apontar para o local d+8(oito bytes além do local da memória de d). Assim, b2aponta para a região dentro dque "parece" uma instância de B2, ou seja, tem o mesmo layout de memória que uma instância de B2.

Invocação

Uma chamada para d->f1()é tratado por dereferencing d's D::B1vpointer, olhando para a f1entrada na tabela de método virtual, e depois dereferencing esse ponteiro para chamar o código.

No caso de herança única (ou em uma linguagem com apenas herança única), se o vpointer for sempre o primeiro elemento em d(como acontece com muitos compiladores), isso se reduz ao seguinte pseudo-C ++:

(*((*d)[0]))(d)

Onde * d se refere à tabela de método virtual de D e [0] se refere ao primeiro método na tabela de método virtual. O parâmetro d torna-se o ponteiro "this" para o objeto.

No caso mais geral, ligar B1::f1()ou D::f2()é mais complicado:

(*(*(d[+0]/*pointer to virtual method table of D (for B1)*/)[0]))(d)   /* Call d->f1() */
(*(*(d[+8]/*pointer to virtual method table of D (for B2)*/)[0]))(d+8) /* Call d->f2() */

A chamada para d-> f1 () passa um ponteiro B1 como um parâmetro. A chamada para d-> f2 () passa um ponteiro B2 como um parâmetro. Esta segunda chamada requer uma correção para produzir o ponteiro correto. A localização de B2 :: f2 não está na tabela de método virtual para D.

Em comparação, uma chamada para d->f0()é muito mais simples:

(*B1::f0)(d)

Eficiência

Uma chamada virtual requer pelo menos uma desreferência indexada extra e, às vezes, uma adição de "correção", em comparação com uma chamada não virtual, que é simplesmente um salto para um ponteiro compilado. Portanto, chamar funções virtuais é inerentemente mais lento do que chamar funções não virtuais. Um experimento feito em 1996 indica que aproximadamente 6–13% do tempo de execução é gasto simplesmente despachando para a função correta, embora a sobrecarga possa chegar a 50%. O custo das funções virtuais pode não ser tão alto em arquiteturas de CPU modernas devido a caches muito maiores e melhor previsão de branch .

Além disso, em ambientes onde a compilação JIT não está em uso, as chamadas de função virtual geralmente não podem ser sequenciais . Em certos casos, pode ser possível para o compilador realizar um processo conhecido como desvirtualização no qual, por exemplo, a consulta e a chamada indireta são substituídas por uma execução condicional de cada corpo embutido, mas tais otimizações não são comuns.

Para evitar essa sobrecarga, os compiladores geralmente evitam usar tabelas de métodos virtuais sempre que a chamada pode ser resolvida em tempo de compilação .

Portanto, a chamada para f1acima pode não exigir uma consulta de tabela porque o compilador pode saber que dsó pode conter um Dneste ponto e Dnão substitui f1. Ou o compilador (ou otimizador) pode ser capaz de detectar que não há subclasses de B1qualquer lugar no programa que sobrescrevam f1. A chamada para B1::f1ou B2::f2provavelmente não exigirá uma pesquisa de tabela porque a implementação é especificada explicitamente (embora ainda exija o ajuste do ponteiro 'this').

Comparação com alternativas

A tabela de método virtual é geralmente uma boa compensação de desempenho para obter despacho dinâmico, mas existem alternativas, como despacho de árvore binária , com desempenho superior, mas custos diferentes.

No entanto, as tabelas de método virtual permitem apenas o despacho único no parâmetro especial "this", em contraste com o despacho múltiplo (como em CLOS , Dylan ou Julia ), onde os tipos de todos os parâmetros podem ser levados em consideração no despacho.

As tabelas de método virtual também funcionam apenas se o despacho for restrito a um conjunto conhecido de métodos, para que possam ser colocados em um array simples construído em tempo de compilação, em contraste com linguagens de digitação duck (como Smalltalk , Python ou JavaScript ).

As linguagens que fornecem um ou ambos esses recursos geralmente são despachadas procurando uma string em uma tabela hash ou algum outro método equivalente. Há uma variedade de técnicas para tornar isso mais rápido (por exemplo, internação / tokenização de nomes de métodos, pesquisas de cache, compilação just-in-time ).

Veja também

Notas

  1. O argumento ^ G ++-fdump-class-hierarchy(começando com a versão 8-fdump-lang-class:) pode ser usado para despejar tabelas de métodos virtuais para inspeção manual. Para o compilador AIX VisualAge XlC, use-qdump_class_hierarchypara despejar a hierarquia de classes e o layout da tabela de função virtual.
  2. ^ https://stackoverflow.com/questions/17960917/why-there-are-two-virtual-destructor-in-the-virtual-table-and-where-is-address-o

Referências

  • Margaret A. Ellis e Bjarne Stroustrup (1990) The Annotated C ++ Reference Manual. Leitura, MA: Addison-Wesley. ( ISBN  0-201-51459-1 )