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 char
membro, 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 arg
for 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 vtable
ou base.vtable
em uma expressão digitada corretamente.
Inside the C++ Object Model
porStanley B. Lippman
. (Seção 4.2, páginas 124-131)