Tabela de métodos virtuais

Na programação de computadores , uma tabela de métodos virtuais ( VMT ), tabela de funções virtuais , tabela de chamadas virtuais , tabela de despacho , vtable ou vftable é um mecanismo usado em uma linguagem de programação para suportar despacho dinâmico (ou ligação de método em tempo de execução ).

Sempre que uma classe define uma função virtual (ou método ), a maioria dos compiladores adiciona uma variável de membro oculta à classe que aponta para uma matriz de ponteiros para funções (virtuais) chamada tabela de métodos virtuais. 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 é implementada por uma classe que herda da classe base.

Há 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 dos 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. O método permite a criação de bibliotecas externas, onde outras técnicas talvez não. [1]

Suponha que um programa contenha três classes em uma hierarquia de herança: uma superclasse , Cat , e duas subclasses , HouseCat e Lion . A classe Cat define uma função virtual chamada speak , portanto suas subclasses podem fornecer uma implementação apropriada (por exemplo, meow ou roar ). Quando o programa chama a função speak em uma referência Cat (que pode se referir a uma instância de Cat ou a uma instância de HouseCat ou 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étodos virtuais de um objeto conterá os endereços dos métodos vinculados dinamicamente do objeto. As chamadas de método são realizadas buscando o endereço do método na tabela de métodos virtuais do objeto. A tabela de métodos virtuais é a mesma para todos os objetos pertencentes à mesma classe e, portanto, normalmente é compartilhada entre eles. Objetos pertencentes a classes compatíveis com tipos (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 tipos. Assim, buscar o endereço do método de um determinado deslocamento em uma tabela de métodos virtual obterá o método correspondente à classe real do objeto. [2]

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

Normalmente, o compilador cria uma tabela de métodos virtuais separada para cada classe. Quando um objeto é criado, um ponteiro para esta tabela, denominado ponteiro de tabela virtual , vpointer ou VPTR , é adicionado como um membro oculto deste 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étodos virtuais 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. [3] Por exemplo, g++ colocou anteriormente o ponteiro no final do objeto. [4]

Exemplo

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

classe B1 { public : virtual ~ B1 () {} void fnonvirtual () {} virtual void f1 () {} int int_in_b1 ; };  

    
    
     
   


classe B2 { público : virtual ~ B2 () {} virtual void f2 () {} int int_in_b2 ; };  

    
     
   

usado para derivar a seguinte classe:

classe D : público B1 , público B2 { público : void d () {} void f2 () override {} int int_in_d ; };       

    
     
   

e o seguinte trecho de código C++:

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

g++ 3.4.6 do GCC produz o seguinte layout de memória de 32 bits para o objeto b2: [nota 1]

b2:
  +0: ​​ponteiro para tabela de métodos virtuais de B2
  +4: valor de int_in_b2

tabela de métodos virtuais de B2:
  +0: ​​B2::f2()   

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

d:
  +0: ​​ponteiro para tabela de métodos virtuais de D (para B1)
  +4: valor de int_in_b1
  +8: ponteiro para tabela de métodos virtuais de D (para B2)
 +12: valor de int_in_b2
 +16: valor de int_in_d

Tamanho total: 20 Bytes.

tabela de métodos virtuais de D (para B1):
  +0: ​​B1::f1() // B1::f1() não é substituído

tabela de métodos virtuais de D (para B2):
  +0: ​​D::f2() // B2::f2() é substituído por D::f2()
                // A localização de B2::f2 não está na tabela de métodos virtuais para D

Observe que aquelas funções que não carregam a palavra-chave virtualem sua declaração (como fnonvirtual()e d()) geralmente não aparecem na tabela de métodos virtuais. Existem exceções para casos especiais apresentados pelo construtor padrão .

Observe também os destruidores virtuais nas classes base B1e B2. Eles são necessários para garantir delete da liberação de 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. [nota 2]

A substituição do método f2() na classe Dé implementada duplicando a tabela de métodos virtuais 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 B2em 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 "consertos de ponteiro", também chamados de thunks , durante a conversão .

Considere o seguinte código C++:

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

Enquanto de b1apontará para o mesmo local de memória após a execução deste código, b2apontará para o local d+8(oito bytes além do local de memória de d). Assim, b2aponta para a região dentro dda qual "se parece" com uma instância de B2, ou seja, tem o mesmo layout de memória que uma instância de B2. [ esclarecimento necessário ]

Invocação

Uma chamada para d->f1()é tratada desreferenciando do vpointer de D::B1, procurando a f1entrada na tabela de métodos virtuais e, em seguida, desreferenciando esse ponteiro para chamar o código.

Herança única

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

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

Onde *dse refere à tabela de métodos virtuais De [0]refere-se ao primeiro método na tabela de métodos virtuais. O parâmetro dse torna o ponteiro "this " para o objeto.

Herança múltipla

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

( * ( * ( d [ 0 ] /*ponteiro para tabela de métodos virtuais de D (para B1)*/ )[ 0 ]))( d ) /* Chamar d->f1() */ ( * ( * ( d [ 8 ] /*ponteiro para tabela de métodos virtuais de D (para B2)*/ )[ 0 ]))( d + 8 ) /* Chamar d->f2() */   
 

A chamada to d->f1()passa um B1ponteiro como parâmetro. A chamada to d->f2()passa um B2ponteiro como parâmetro. Esta segunda chamada requer uma correção para produzir o ponteiro correto. A localização de B2::f2não está na tabela de métodos virtuais para D.

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

( * B1 :: fnonvirtual )( d )

Eficiência

Uma chamada virtual requer pelo menos uma desreferência indexada extra e, às vezes, uma adição de "conserto", 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%. [5] O custo das funções virtuais pode não ser tão alto nas arquiteturas de CPU modernas devido aos caches muito maiores e à melhor previsão de ramificação .

Além disso, em ambientes onde a compilação JIT não está em uso, as chamadas de funções virtuais geralmente não podem ser incorporadas . Em certos casos, pode ser possível que o compilador execute 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 o uso de tabelas de métodos virtuais sempre que a chamada pode ser resolvida em tempo de compilação .

Assim, a chamada f1acima pode não exigir uma consulta de tabela porque o compilador pode saber que dsó pode conter a Dneste ponto e Dnão substitui f1. Ou o compilador (ou otimizador) pode detectar que não há subclasses em B1nenhum lugar do programa que substituam f1. A chamada para B1::f1or B2::f2provavelmente não exigirá uma consulta de tabela porque a implementação é especificada explicitamente (embora ainda exija a correção do ponteiro 'this').

Comparação com alternativas

A tabela de métodos virtuais geralmente é uma boa compensação de desempenho para obter o despacho dinâmico, mas existem alternativas, como o despacho de árvore binária, com desempenho superior em alguns casos típicos, mas com diferentes compensações. [1] [6]

No entanto, as tabelas de métodos virtuais 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étodos virtuais também funcionam apenas se o despacho estiver 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 de pato (como Smalltalk , Python ou JavaScript ).

Idiomas que fornecem um ou ambos esses recursos geralmente são despachados pesquisando 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 do G++ -fdump-class-hierarchy(começando com a versão 8:) -fdump-lang-classpode ser usado para despejar tabelas de métodos virtuais para inspeção manual. Para o compilador AIX VisualAge XlC, use -qdump_class_hierarchypara fazer dump da hierarquia de classes e do layout da tabela de funções virtuais.
  2. ^ "C++ - por que existem dois destruidores virtuais na tabela virtual e onde está o endereço da função não virtual (gcc4.6.3)" .

Referências

  1. ^ ab Zendra, Olivier; Colnet, Dominique; Collin, Suzanne (1997). Despacho Dinâmico Eficiente sem Tabelas de Funções Virtuais: O Compilador SmallEiffel - 12ª Conferência Anual ACM SIGPLAN sobre Sistemas, Linguagens e Aplicações de Programação Orientada a Objetos (OOPSLA'97), ACM SIGPLAN, outubro de 1997, Atlanta, Estados Unidos. pp.125-141. inria-00565627. Centro de Pesquisa em Informática de Nancy Campus Scientifique, Bâtiment LORIA. pág. 16.
  2. ^ Ellis & Stroustrup 1990, pp.
  3. ^ Danny Kalev. "Guia de referência C++: O modelo de objeto II". 2003. Título "Herança e Polimorfismo" e "Herança Múltipla".
  4. ^ "Problemas fechados de C++ ABI" . Arquivado do original em 25 de julho de 2011 . Recuperado em 17 de junho de 2011 .{{cite web}}: CS1 maint: bot: status do URL original desconhecido ( link )
  5. ^ Driesen, Karel e Hölzle, Urs, "O custo direto das chamadas de funções virtuais em C++", OOPSLA 1996
  6. ^ Zendra, Olivier e Driesen, Karel, "Stress-testing Control Structures for Dynamic Dispatch in Java", pp. 105–118, Proceedings of the USENIX 2nd Java Virtual Machine Research and Technology Symposium, 2002 (JVM '02)
Obtido em "https://en.wikipedia.org/w/index.php?title=Virtual_method_table&oldid=1183772122"