Como as funções virtuais e vtable são implementadas?


109

Todos nós sabemos o que são funções virtuais em C ++, mas como elas são implementadas em um nível profundo?

A vtable pode ser modificada ou mesmo acessada diretamente em tempo de execução?

A vtable existe para todas as classes ou apenas para aquelas que têm pelo menos uma função virtual?

As classes abstratas simplesmente têm um NULL para o ponteiro de função de pelo menos uma entrada?

Ter uma única função virtual desacelera toda a classe? Ou apenas a chamada para a função que é virtual? E a velocidade é afetada se a função virtual for realmente substituída ou não, ou isso não tem efeito enquanto for virtual.


2
Sugira a leitura da obra-prima Inside the C++ Object Modelpor Stanley B. Lippman. (Seção 4.2, páginas 124-131)
smwikipedia

Respostas:


123

Como as funções virtuais são implementadas em um nível profundo?

De "Funções Virtuais em C ++" :

Sempre que um programa tem uma função virtual declarada, uma tabela é construída para a classe. A tabela v consiste em endereços para as funções virtuais para classes que contêm uma ou mais funções virtuais. O objeto da classe que contém a função virtual contém um ponteiro virtual que aponta para o endereço base da tabela virtual na memória. Sempre que houver uma chamada de função virtual, a tabela v é usada para resolver para o endereço da função. Um objeto da classe que contém uma ou mais funções virtuais contém um ponteiro virtual chamado vptr bem no início do objeto na memória. Portanto, o tamanho do objeto, neste caso, aumenta com o tamanho do ponteiro. Este vptr contém o endereço base da tabela virtual na memória. Observe que as tabelas virtuais são específicas da classe, ou seja, existe apenas uma tabela virtual para uma classe, independentemente do número de funções virtuais que ela contém. Essa tabela virtual, por sua vez, contém os endereços de base de uma ou mais funções virtuais da classe. No momento em que uma função virtual é chamada em um objeto, o vptr desse objeto fornece o endereço base da tabela virtual para essa classe na memória. Esta tabela é usada para resolver a chamada de função, pois contém os endereços de todas as funções virtuais dessa classe. É assim que a vinculação dinâmica é resolvida durante uma chamada de função virtual. o vptr desse objeto fornece o endereço base da tabela virtual para essa classe na memória. Esta tabela é usada para resolver a chamada de função, pois contém os endereços de todas as funções virtuais dessa classe. É assim que a vinculação dinâmica é resolvida durante uma chamada de função virtual. o vptr desse objeto fornece o endereço base da tabela virtual para essa classe na memória. Esta tabela é usada para resolver a chamada de função, pois contém os endereços de todas as funções virtuais dessa classe. É assim que a vinculação dinâmica é resolvida durante uma chamada de função virtual.

A vtable pode ser modificada ou mesmo acessada diretamente em tempo de execução?

Universalmente, acredito que a resposta seja "não". Você poderia alterar a memória para encontrar a vtable, mas ainda não saberia como é a assinatura da função ao chamá-la. Qualquer coisa que você deseja alcançar com essa capacidade (que a linguagem suporta) deve ser possível sem acessar a vtable diretamente ou modificá-la em tempo de execução. Observe também que a especificação da linguagem C ++ não especifica que vtables são necessários - no entanto, é assim que a maioria dos compiladores implementa funções virtuais.

A vtable existe para todos os objetos ou apenas aqueles que têm pelo menos uma função virtual?

Eu acredito que a resposta aqui é "depende da implementação", uma vez que a especificação não requer vtables em primeiro lugar. No entanto, na prática, acredito que todos os compiladores modernos só criam uma vtable se uma classe tiver pelo menos 1 função virtual. Há uma sobrecarga de espaço associada à vtable e uma sobrecarga de tempo associada à chamada de uma função virtual versus uma função não virtual.

As classes abstratas simplesmente têm um NULL para o ponteiro de função de pelo menos uma entrada?

A resposta é que não é especificado pela especificação do idioma, portanto, depende da implementação. Chamar a função virtual pura resulta em comportamento indefinido se não estiver definido (o que geralmente não é) (ISO / IEC 14882: 2003 10.4-2). Na prática, ele aloca um slot na vtable para a função, mas não atribui um endereço a ela. Isso deixa a vtable incompleta, o que requer que as classes derivadas implementem a função e concluam a vtable. Algumas implementações simplesmente colocam um ponteiro NULL na entrada vtable; outras implementações colocam um ponteiro para um método fictício que faz algo semelhante a uma asserção.

Observe que uma classe abstrata pode definir uma implementação para uma função virtual pura, mas essa função só pode ser chamada com uma sintaxe de id qualificada (ou seja, especificando totalmente a classe no nome do método, semelhante a chamar um método de classe base de um classe derivada). Isso é feito para fornecer uma implementação padrão fácil de usar, enquanto ainda requer que uma classe derivada forneça uma substituição.

Ter uma única função virtual desacelera toda a classe ou apenas a chamada para a função que é virtual?

Isto está chegando ao limite do meu conhecimento, então alguém por favor me ajude aqui se eu estiver errado!

Eu acredito que apenas as funções que são virtuais na classe experimentam o impacto do tempo de desempenho relacionado à chamada de uma função virtual em vez de uma função não virtual. A sobrecarga de espaço para a classe existe de qualquer maneira. Observe que se houver uma vtable, haverá apenas 1 por classe , e não uma por objeto .

A velocidade é afetada se a função virtual for realmente substituída ou não, ou isso não tem efeito, desde que seja virtual?

Não acredito que o tempo de execução de uma função virtual que é substituída diminui em comparação com a chamada da função virtual base. No entanto, há uma sobrecarga de espaço adicional para a classe associada à definição de outra vtable para a classe derivada versus a classe base.

Recursos adicionais:

http://www.codersource.net/published/view/325/virtual_functions_in.aspx (via máquina de volta)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/ cxx-abi / abi.html # vtable


2
Não estaria de acordo com a filosofia C ++ de Stroustrup para um compilador colocar um ponteiro vtable desnecessário em um objeto que não precisa dele. A regra é que você não obtém sobrecarga que não esteja em C, a menos que você peça por isso, e é rude os compiladores quebrarem isso.
Steve Jessop

3
Concordo que seria tolice para qualquer compilador que se leva a sério usar uma vtable quando não existem funções virtuais. No entanto, achei importante ressaltar que, até onde sei, o padrão C ++ não o exige / exige, portanto, esteja avisado antes de depender dele.
Zach Burlingame

8
Mesmo as funções virtuais podem ser chamadas não virtualmente. Na verdade, isso é bastante comum: se o objeto estiver na pilha, dentro do escopo, o compilador saberá o tipo exato e otimizará a pesquisa vtable. Isso é especialmente verdadeiro para o dtor, que deve ser chamado no mesmo escopo de pilha.
MSalters

1
Acredito que quando uma classe tem pelo menos uma função virtual, todo objeto tem uma vtable, e não uma para a classe inteira.
Asaf R

3
Implementação comum: cada objeto possui um ponteiro para uma vtable; a classe possui a mesa. A mágica da construção consiste simplesmente em atualizar o ponteiro vtable no ctor derivado, depois que o ctor base terminar.
MSalters

31
  • A vtable pode ser modificada ou mesmo acessada diretamente em tempo de execução?

Não portável, mas se você não se importa com truques sujos, com certeza!

AVISO : Esta técnica não é recomendada para uso por crianças, adultos com idade inferior a 969 ou pequenas criaturas peludas de Alpha Centauri. Os efeitos colaterais podem incluir demônios que voam para fora do seu nariz , o aparecimento abrupto de Yog-Sothoth como um aprovador obrigatório em todas as revisões de código subsequentes ou a adição retroativa de IHuman::PlayPiano()a todas as instâncias existentes]

Na maioria dos compiladores que já vi, o vtbl * são os primeiros 4 bytes do objeto, e o conteúdo do vtbl é simplesmente um array de ponteiros de membro (geralmente na ordem em que foram declarados, com a classe base primeiro). É claro que existem outros layouts possíveis, mas é o que geralmente observei.

class A {
  public:
  virtual int f1() = 0;
};
class B : public A {
  public:
  virtual int f1() { return 1; }
  virtual int f2() { return 2; }
};
class C : public A {
  public:
  virtual int f1() { return -1; }
  virtual int f2() { return -2; }
};

A *x = new B;
A *y = new C;
A *z = new C;

Agora, para puxar algumas travessuras ...

Alterando a classe em tempo de execução:

std::swap(*(void **)x, *(void **)y);
// Now x is a C, and y is a B! Hope they used the same layout of members!

Substituindo um método para todas as instâncias (monkeypatching uma classe)

Este é um pouco mais complicado, pois o próprio vtbl provavelmente está na memória somente leitura.

int f3(A*) { return 0; }

mprotect(*(void **)x,8,PROT_READ|PROT_WRITE|PROT_EXEC);
// Or VirtualProtect on win32; this part's very OS-specific
(*(int (***)(A *)x)[0] = f3;
// Now C::f1() returns 0 (remember we made x into a C above)
// so x->f1() and z->f1() both return 0

É muito provável que o último faça os verificadores de vírus e o link despertar e notar, devido às manipulações do mprotect. Em um processo que usa o bit NX, ele pode falhar.


6
Hmm. Parece sinistro que isso tenha recebido uma recompensa. Espero que isso não signifique que @Mobilewits pense que tais travessuras são na verdade uma boa ideia ...
puetzk

1
Por favor, considere desencorajar o uso desta técnica, de forma clara e forte, ao invés de "piscar".
einpoklum de

" conteúdo vtbl é simplesmente uma matriz de ponteiros de membro " na verdade, é um registro (uma estrutura) com entradas diferentes, que por acaso estão espaçadas uniformemente
cara curioso

1
Você pode olhar para isso de qualquer maneira; os ponteiros de função têm assinaturas diferentes e, portanto, tipos de ponteiro diferentes; nesse sentido, é realmente semelhante a uma estrutura. Mas em outros contextos, mas a ideia de índice vtbl é útil (por exemplo, ActiveX o usa da maneira que descreve interfaces duplas em typelibs), que é uma visão mais parecida com array.
puetzk

17

Ter uma única função virtual desacelera toda a classe?

Ou apenas a chamada para a função que é virtual? E a velocidade é afetada se a função virtual for realmente substituída ou não, ou isso não tem efeito enquanto for virtual.

Ter funções virtuais torna toda a classe mais lenta, na medida em que mais um item de dados precisa ser inicializado, copiado, ... ao lidar com um objeto de tal classe. Para uma classe com cerca de meia dúzia de membros, a diferença deve ser desprezível. Para uma classe que contém apenas um único charmembro, ou nenhum membro, a diferença pode ser notável.

Além disso, é importante notar que nem toda chamada para uma função virtual é uma chamada de função virtual. Se você tiver um objeto de um tipo conhecido, o compilador pode emitir código para uma chamada de função normal e pode até embutir essa função, se assim o desejar. É apenas quando você faz chamadas polimórficas, por meio de um ponteiro ou referência que pode apontar para um objeto da classe base ou para um objeto de alguma classe derivada, que você precisa da indireção vtable e paga por isso em termos de desempenho.

struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
  Foo x; x.a(); // non-virtual: always calls Foo::a()
  Bar y; y.a(); // non-virtual: always calls Bar::a()
  arg.a();      // virtual: must dispatch via vtable
  Foo z = arg;  // copy constructor Foo::Foo(const Foo&) will convert to Foo
  z.a();        // non-virtual Foo::a, since z is a Foo, even if arg was not
}

As etapas que o hardware deve realizar são essencialmente as mesmas, independentemente de a função ser substituída ou não. O endereço da vtable é lido do objeto, o ponteiro de função recuperado do slot apropriado e a função chamada por ponteiro. Em termos de desempenho real, as previsões do ramo podem ter algum impacto. Portanto, por exemplo, se a maioria de seus objetos se referem à mesma implementação de uma determinada função virtual, então há alguma chance de que o preditor de ramificação preveja corretamente qual função chamar antes mesmo que o ponteiro seja recuperado. Mas não importa qual função é a comum: pode ser a maioria dos objetos delegando ao caso base não sobrescrito ou a maioria dos objetos pertencentes à mesma subclasse e, portanto, delegando ao mesmo caso sobrescrito.

como eles são implementados em um nível profundo?

Eu gosto da ideia de jheriko para demonstrar isso usando uma implementação simulada. Mas eu usaria C para implementar algo semelhante ao código acima, para que o nível baixo seja mais facilmente visto.

classe pai Foo

typedef struct Foo_t Foo;   // forward declaration
struct slotsFoo {           // list all virtual functions of Foo
  const void *parentVtable; // (single) inheritance
  void (*destructor)(Foo*); // virtual destructor Foo::~Foo
  int (*a)(Foo*);           // virtual function Foo::a
};
struct Foo_t {                      // class Foo
  const struct slotsFoo* vtable;    // each instance points to vtable
};
void destructFoo(Foo* self) { }     // Foo::~Foo
int aFoo(Foo* self) { return 1; }   // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
  0,                                // no parent class
  destructFoo,
  aFoo
};
void constructFoo(Foo* self) {      // Foo::Foo()
  self->vtable = &vtableFoo;        // object points to class vtable
}
void copyConstructFoo(Foo* self,
                      Foo* other) { // Foo::Foo(const Foo&)
  self->vtable = &vtableFoo;        // don't copy from other!
}

classe derivada Bar

typedef struct Bar_t {              // class Bar
  Foo base;                         // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { }     // Bar::~Bar
int aBar(Bar* self) { return 2; }   // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
  &vtableFoo,                       // can dynamic_cast to Foo
  (void(*)(Foo*)) destructBar,      // must cast type to avoid errors
  (int(*)(Foo*)) aBar
};
void constructBar(Bar* self) {      // Bar::Bar()
  self->base.vtable = &vtableBar;   // point to Bar vtable
}

função f realizando chamada de função virtual

void f(Foo* arg) {                  // same functionality as above
  Foo x; constructFoo(&x); aFoo(&x);
  Bar y; constructBar(&y); aBar(&y);
  arg->vtable->a(arg);              // virtual function call
  Foo z; copyConstructFoo(&z, arg);
  aFoo(&z);
  destructFoo(&z);
  destructBar(&y);
  destructFoo(&x);
}

Como você pode ver, uma vtable é apenas um bloco estático na memória, contendo principalmente ponteiros de função. Cada objeto de uma classe polimórfica apontará para a vtable correspondente ao seu tipo dinâmico. Isso também torna a conexão entre o RTTI e as funções virtuais mais clara: você pode verificar o tipo de classe simplesmente olhando para qual vtable ela aponta. O texto acima é simplificado de várias maneiras, como, por exemplo, herança múltipla, mas o conceito geral é válido.

Se argfor do tipo Foo*e você pegar arg->vtable, mas na verdade for um objeto do tipo Bar, você ainda obterá o endereço correto do vtable. Isso porque vtableé sempre o primeiro elemento no endereço do objeto, independentemente de ser chamado vtableou base.vtableem uma expressão digitada corretamente.


"Cada objeto de uma classe polimórfica apontará para sua própria vtable." Você está dizendo que cada objeto tem sua própria vtable? AFAIK vtable é compartilhada entre todos os objetos da mesma classe. Me avise se eu estiver errado.
Bhuwan

1
@Bhuwan: Não, você está certo: há apenas uma vtable por tipo (que pode ser por instanciação de template no caso de templates). Eu quis dizer que cada objeto de uma classe polimórfica aponta para a vtable que se aplica a ele, então cada objeto tem esse ponteiro, mas para objetos do mesmo tipo ele aponta para a mesma tabela. Provavelmente eu deveria reformular isso.
MvG de

1
@MvG " objetos do mesmo tipo apontarão para a mesma tabela " não durante a construção de classes base com classes base virtuais! (um caso muito especial)
cara curioso de

1
@curiousguy: Eu arquivaria isso em “o acima é simplificado de várias maneiras”, particularmente porque a principal aplicação das bases virtuais é a herança múltipla, que eu também não modelei. Mas obrigado pelo comentário, é útil tê-lo aqui para pessoas que precisam de mais detalhes.
MvG de

3

Normalmente com uma VTable, uma matriz de ponteiros para funções.


2

Esta resposta foi incorporada à resposta do Community Wiki

  • As classes abstratas simplesmente têm um NULL para o ponteiro de função de pelo menos uma entrada?

A resposta para isso é que ela não é especificada - chamar a função virtual pura resulta em um comportamento indefinido se ela não for definida (o que geralmente não é) (ISO / IEC 14882: 2003 10.4-2). Algumas implementações simplesmente colocam um ponteiro NULL na entrada vtable; outras implementações colocam um ponteiro para um método fictício que faz algo semelhante a uma asserção.

Observe que uma classe abstrata pode definir uma implementação para uma função virtual pura, mas essa função só pode ser chamada com uma sintaxe de id qualificada (ou seja, especificando totalmente a classe no nome do método, semelhante a chamar um método de classe base de um classe derivada). Isso é feito para fornecer uma implementação padrão fácil de usar, enquanto ainda requer que uma classe derivada forneça uma substituição.


Além disso, não acho que uma classe abstrata possa definir uma implementação para uma função virtual pura. Por definição, uma função virtual pura não tem corpo (por exemplo, bool my_func () = 0;). No entanto, você pode fornecer implementações para funções virtuais regulares.
Zach Burlingame

Uma função virtual pura pode ter uma definição. Consulte Scott Meyers '"Effective C ++, 3rd Ed" Item # 34, ISO 14882-2003 10.4-2, ou bytes.com/forum/thread572745.html
Michael Burr

2

Você pode recriar a funcionalidade de funções virtuais em C ++ usando ponteiros de função como membros de uma classe e funções estáticas como as implementações, ou usando ponteiros para funções de membro e funções de membro para as implementações. Existem apenas vantagens de notação entre os dois métodos ... na verdade, as chamadas de função virtuais são apenas uma conveniência de notação em si. Na verdade, a herança é apenas uma notação conveniente ... tudo pode ser implementado sem usar os recursos da linguagem para herança. :)

O código abaixo é uma porcaria não testado, provavelmente um código com bugs, mas espero que demonstre a ideia.

por exemplo

class Foo
{
protected:
 void(*)(Foo*) MyFunc;
public:
 Foo() { MyFunc = 0; }
 void ReplciatedVirtualFunctionCall()
 {
  MyFunc(*this);
 }
...
};

class Bar : public Foo
{
private:
 static void impl1(Foo* f)
 {
  ...
 }
public:
 Bar() { MyFunc = impl1; }
...
};

class Baz : public Foo
{
private:
 static void impl2(Foo* f)
 {
  ...
 }
public:
 Baz() { MyFunc = impl2; }
...
};

void(*)(Foo*) MyFunc;isso é alguma sintaxe Java?
curioso

não, sua sintaxe C / C ++ para ponteiros de função. Para me citar "Você pode recriar a funcionalidade de funções virtuais em C ++ usando ponteiros de função". é uma sintaxe desagradável, mas algo com que se familiarizar se você se considera um programador C.
jheriko de

um ponteiro de função ac seria mais semelhante a: int ( PROC) (); e um ponteiro para uma função de membro de classe seria semelhante a: int (ClassName :: MPROC) ();
Ameaça de

1
@menace, você esqueceu alguma sintaxe aí ... você está pensando no typedef talvez? typedef int (* PROC) (); então você pode apenas fazer PROC foo mais tarde em vez de int (* foo) ()?
jheriko

2

Vou tentar tornar isso simples :)

Todos nós sabemos o que são funções virtuais em C ++, mas como elas são implementadas em um nível profundo?

Este é um array com ponteiros para funções, que são implementações de uma função virtual particular. Um índice nesta matriz representa um índice particular de uma função virtual definida para uma classe. Isso inclui funções virtuais puras.

Quando uma classe polimórfica deriva de outra classe polimórfica, podemos ter as seguintes situações:

  • A classe derivada não adiciona novas funções virtuais nem substitui nenhuma. Nesse caso, essa classe compartilha a vtable com a classe base.
  • A classe derivada adiciona e substitui métodos virtuais. Nesse caso, ele obtém sua própria vtable, onde as funções virtuais adicionadas têm índice começando após o último derivado.
  • Várias classes polimórficas na herança. Neste caso, temos uma mudança de índice entre a segunda e a próxima base e o índice dela na classe derivada

A vtable pode ser modificada ou mesmo acessada diretamente em tempo de execução?

Não é o caminho padrão - não há API para acessá-los. Os compiladores podem ter algumas extensões ou APIs privadas para acessá-los, mas isso pode ser apenas uma extensão.

A vtable existe para todas as classes ou apenas para aquelas que têm pelo menos uma função virtual?

Apenas aquelas que possuem pelo menos uma função virtual (seja ela mesmo destrutora) ou derivam pelo menos uma classe que possui sua vtable ("é polimórfica").

As classes abstratas simplesmente têm um NULL para o ponteiro de função de pelo menos uma entrada?

Essa é uma implementação possível, mas não praticada. Em vez disso, geralmente há uma função que imprime algo como "função virtual pura chamada" e o faz abort(). A chamada para que pode ocorrer se você tentar chamar o método abstrato no construtor ou destruidor.

Ter uma única função virtual desacelera toda a classe? Ou apenas a chamada para a função que é virtual? E a velocidade é afetada se a função virtual for realmente substituída ou não, ou isso não tem efeito enquanto for virtual.

A lentidão depende apenas se a chamada foi resolvida como uma chamada direta ou como uma chamada virtual. E nada mais importa. :)

Se você chamar uma função virtual por meio de um ponteiro ou referência a um objeto, ela sempre será implementada como uma chamada virtual - porque o compilador nunca pode saber que tipo de objeto será atribuído a esse ponteiro em tempo de execução, e se é de um classe na qual este método é substituído ou não. Apenas em dois casos o compilador pode resolver a chamada para uma função virtual como uma chamada direta:

  • Se você chamar o método por meio de um valor (uma variável ou resultado de uma função que retorna um valor) - neste caso, o compilador não tem dúvidas de qual é a classe real do objeto e pode "resolvê-la de maneira rígida" em tempo de compilação .
  • Se o método virtual for declarado finalna classe para a qual você tem um ponteiro ou referência por meio da qual você o chama ( somente em C ++ 11 ). Nesse caso, o compilador sabe que esse método não pode sofrer nenhuma modificação adicional e só pode ser o método desta classe.

Observe, porém, que as chamadas virtuais têm apenas sobrecarga de desreferenciar dois ponteiros. Usar RTTI (embora disponível apenas para classes polimórficas) é mais lento do que chamar métodos virtuais, caso você encontre um caso para implementar a mesma coisa dessas duas maneiras. Por exemplo, definir virtual bool HasHoof() { return false; }e substituir apenas o bool Horse::HasHoof() { return true; }que forneceria a capacidade de chamar if (anim->HasHoof())isso será mais rápido do que tentar if(dynamic_cast<Horse*>(anim)). Isso ocorre porque dynamic_cast, em alguns casos, é necessário percorrer a hierarquia de classes, mesmo recursivamente, para ver se pode ser construído o caminho a partir do tipo de ponteiro real e do tipo de classe desejado. Enquanto a chamada virtual é sempre a mesma - desreferenciando dois ponteiros.


2

Aqui está uma implementação manual executável de tabela virtual em C ++ moderno. Tem uma semântica bem definida, sem hacks e não void*.

Observação: .*e ->*são operadores diferentes de *e ->. Os ponteiros de função de membro funcionam de maneira diferente.

#include <iostream>
#include <vector>
#include <memory>

struct vtable; // forward declare, we need just name

class animal
{
public:
    const std::string& get_name() const { return name; }

    // these will be abstract
    bool has_tail() const;
    bool has_wings() const;
    void sound() const;

protected: // we do not want animals to be created directly
    animal(const vtable* vtable_ptr, std::string name)
    : vtable_ptr(vtable_ptr), name(std::move(name)) { }

private:
    friend vtable; // just in case for non-public methods

    const vtable* const vtable_ptr;
    std::string name;
};

class cat : public animal
{
public:
    cat(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does meow\n"; 
    }
};

class dog : public animal
{
public:
    dog(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does whoof\n"; 
    }
};

class parrot : public animal
{
public:
    parrot(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return false; }
    bool has_wings() const { return true; }
    void sound() const
    {
        std::cout << get_name() << " does crrra\n"; 
    }
};

// now the magic - pointers to member functions!
struct vtable
{
    bool (animal::* const has_tail)() const;
    bool (animal::* const has_wings)() const;
    void (animal::* const sound)() const;

    // constructor
    vtable (
        bool (animal::* const has_tail)() const,
        bool (animal::* const has_wings)() const,
        void (animal::* const sound)() const
    ) : has_tail(has_tail), has_wings(has_wings), sound(sound) { }
};

// global vtable objects
const vtable vtable_cat(
    static_cast<bool (animal::*)() const>(&cat::has_tail),
    static_cast<bool (animal::*)() const>(&cat::has_wings),
    static_cast<void (animal::*)() const>(&cat::sound));
const vtable vtable_dog(
    static_cast<bool (animal::*)() const>(&dog::has_tail),
    static_cast<bool (animal::*)() const>(&dog::has_wings),
    static_cast<void (animal::*)() const>(&dog::sound));
const vtable vtable_parrot(
    static_cast<bool (animal::*)() const>(&parrot::has_tail),
    static_cast<bool (animal::*)() const>(&parrot::has_wings),
    static_cast<void (animal::*)() const>(&parrot::sound));

// set vtable pointers in constructors
cat::cat(std::string name) : animal(&vtable_cat, std::move(name)) { }
dog::dog(std::string name) : animal(&vtable_dog, std::move(name)) { }
parrot::parrot(std::string name) : animal(&vtable_parrot, std::move(name)) { }

// implement dynamic dispatch
bool animal::has_tail() const
{
    return (this->*(vtable_ptr->has_tail))();
}

bool animal::has_wings() const
{
    return (this->*(vtable_ptr->has_wings))();
}

void animal::sound() const
{
    (this->*(vtable_ptr->sound))();
}

int main()
{
    std::vector<std::unique_ptr<animal>> animals;
    animals.push_back(std::make_unique<cat>("grumpy"));
    animals.push_back(std::make_unique<cat>("nyan"));
    animals.push_back(std::make_unique<dog>("doge"));
    animals.push_back(std::make_unique<parrot>("party"));

    for (const auto& a : animals)
        a->sound();

    // note: destructors are not dispatched virtually
}

1

Cada objeto tem um ponteiro vtable que aponta para uma matriz de funções-membro.


1

Algo não mencionado aqui em todas essas respostas é no caso de herança múltipla, onde todas as classes base possuem métodos virtuais. A classe herdada possui vários ponteiros para um vmt. O resultado é que o tamanho de cada instância de tal objeto é maior. Todo mundo sabe que uma classe com métodos virtuais tem 4 bytes extras para o vmt, mas no caso de herança múltipla é para cada classe base que tem métodos virtuais vezes 4. 4 sendo o tamanho do ponteiro.


0

As respostas de Burly estão corretas aqui, exceto pela pergunta:

As classes abstratas simplesmente têm um NULL para o ponteiro de função de pelo menos uma entrada?

A resposta é que nenhuma tabela virtual é criada para classes abstratas. Não há necessidade, pois nenhum objeto dessas classes pode ser criado!

Em outras palavras, se tivermos:

class B { ~B() = 0; }; // Abstract Base class
class D : public B { ~D() {} }; // Concrete Derived class

D* pD = new D();
B* pB = pD;

O ponteiro vtbl acessado por meio de pB será o vtbl da classe D. É exatamente assim que o polimorfismo é implementado. Ou seja, como os métodos D são acessados ​​por meio do pB. Não há necessidade de um vtbl para a classe B.

Em resposta ao comentário de Mike abaixo ...

Se a classe B em minha descrição tem um método virtual foo () que não é sobrescrito por D e um método virtual bar () que é sobrescrito, então o vtbl de D terá um ponteiro para foo de B () e para sua própria barra () . Ainda não há vtbl criado para B.


Isso não está correto por 2 motivos: 1) uma classe abstrata pode ter métodos virtuais regulares além dos métodos virtuais puros e 2) métodos virtuais puros podem opcionalmente ter uma definição que pode ser chamada com um nome totalmente qualificado.
Michael Burr

Certo - pensando bem, imagino que, se todos os métodos virtuais fossem puramente virtuais, o compilador poderia otimizar a vtable (seria necessário ajudar a formar o vinculador para garantir que não houvesse definições também).
Michael Burr

1
A resposta é que nenhuma tabela virtual é criada para classes abstratas. ” Errado. “ Não há necessidade, pois nenhum objeto dessas classes pode ser criado! ” Errado.
curioso

Posso seguir seu raciocínio de que nenhuma tabela para B deve ser necessária. Só porque alguns de seus métodos têm implementações (padrão), não significa que eles precisam ser armazenados em uma vtable. Mas acabei de executar seu código (modulo algumas correções para torná-lo compilado) gcc -Sseguido por c++filte há claramente uma vtable para Bincluída lá. Acho que pode ser porque a vtable também armazena dados RTTI, como nomes de classe e herança. Pode ser necessário para um dynamic_cast<B*>. Mesmo -fno-rttinão faz o vtable ir embora. Com, em clang -O3vez de gcc, de repente se foi.
MvG

@MvG " Só porque alguns de seus métodos têm implementações (padrão), não significa que eles tenham que ser armazenados em uma vtable " Sim, significa exatamente isso.
curioso

0

prova de conceito muito fofa que fiz um pouco antes (para ver se a ordem de herança é importante); deixe-me saber se sua implementação de C ++ realmente o rejeita (minha versão do gcc só dá um aviso para atribuir estruturas anônimas, mas isso é um bug), estou curioso.

CCPolite.h :

#ifndef CCPOLITE_H
#define CCPOLITE_H

/* the vtable or interface */
typedef struct {
    void (*Greet)(void *);
    void (*Thank)(void *);
} ICCPolite;

/**
 * the actual "object" literal as C++ sees it; public variables be here too 
 * all CPolite objects use(are instances of) this struct's structure.
 */
typedef struct {
    ICCPolite *vtbl;
} CPolite;

#endif /* CCPOLITE_H */

CCPolite_constructor.h :

/** 
 * unconventionally include me after defining OBJECT_NAME to automate
 * static(allocation-less) construction.
 *
 * note: I assume CPOLITE_H is included; since if I use anonymous structs
 *     for each object, they become incompatible and cause compile time errors
 *     when trying to do stuff like assign, or pass functions.
 *     this is similar to how you can't pass void * to windows functions that
 *         take handles; these handles use anonymous structs to make 
 *         HWND/HANDLE/HINSTANCE/void*/etc not automatically convertible, and
 *         require a cast.
 */
#ifndef OBJECT_NAME
    #error CCPolite> constructor requires object name.
#endif

CPolite OBJECT_NAME = {
    &CCPolite_Vtbl
};

/* ensure no global scope pollution */
#undef OBJECT_NAME

main.c :

#include <stdio.h>
#include "CCPolite.h"

// | A Greeter is capable of greeting; nothing else.
struct IGreeter
{
    virtual void Greet() = 0;
};

// | A Thanker is capable of thanking; nothing else.
struct IThanker
{
    virtual void Thank() = 0;
};

// | A Polite is something that implements both IGreeter and IThanker
// | Note that order of implementation DOES MATTER.
struct IPolite1 : public IGreeter, public IThanker{};
struct IPolite2 : public IThanker, public IGreeter{};

// | implementation if IPolite1; implements IGreeter BEFORE IThanker
struct CPolite1 : public IPolite1
{
    void Greet()
    {
        puts("hello!");
    }

    void Thank()
    {
        puts("thank you!");
    }
};

// | implementation if IPolite1; implements IThanker BEFORE IGreeter
struct CPolite2 : public IPolite2
{
    void Greet()
    {
        puts("hi!");
    }

    void Thank()
    {
        puts("ty!");
    }
};

// | imposter Polite's Greet implementation.
static void CCPolite_Greet(void *)
{
    puts("HI I AM C!!!!");
}

// | imposter Polite's Thank implementation.
static void CCPolite_Thank(void *)
{
    puts("THANK YOU, I AM C!!");
}

// | vtable of the imposter Polite.
ICCPolite CCPolite_Vtbl = {
    CCPolite_Thank,
    CCPolite_Greet    
};

CPolite CCPoliteObj = {
    &CCPolite_Vtbl
};

int main(int argc, char **argv)
{
    puts("\npart 1");
    CPolite1 o1;
    o1.Greet();
    o1.Thank();

    puts("\npart 2");    
    CPolite2 o2;    
    o2.Greet();
    o2.Thank();    

    puts("\npart 3");    
    CPolite1 *not1 = (CPolite1 *)&o2;
    CPolite2 *not2 = (CPolite2 *)&o1;
    not1->Greet();
    not1->Thank();
    not2->Greet();
    not2->Thank();

    puts("\npart 4");        
    CPolite1 *fake = (CPolite1 *)&CCPoliteObj;
    fake->Thank();
    fake->Greet();

    puts("\npart 5");        
    CPolite2 *fake2 = (CPolite2 *)fake;
    fake2->Thank();
    fake2->Greet();

    puts("\npart 6");        
    #define OBJECT_NAME fake3
    #include "CCPolite_constructor.h"
    fake = (CPolite1 *)&fake3;
    fake->Thank();
    fake->Greet();

    puts("\npart 7");        
    #define OBJECT_NAME fake4
    #include "CCPolite_constructor.h"
    fake2 = (CPolite2 *)&fake4;
    fake2->Thank();
    fake2->Greet();    

    return 0;
}

resultado:

part 1
hello!
thank you!

part 2
hi!
ty!

part 3
ty!
hi!
thank you!
hello!

part 4
HI I AM C!!!!
THANK YOU, I AM C!!

part 5
THANK YOU, I AM C!!
HI I AM C!!!!

part 6
HI I AM C!!!!
THANK YOU, I AM C!!

part 7
THANK YOU, I AM C!!
HI I AM C!!!!

observe que, como nunca estou alocando meu objeto falso, não há necessidade de fazer qualquer destruição; os destruidores são colocados automaticamente no final do escopo dos objetos alocados dinamicamente para recuperar a memória do próprio literal do objeto e do ponteiro vtable.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.