¿Cuándo se crea un vtable en C++?
polymorphism virtual-functions (6)
¿Cuándo exactamente el compilador crea una tabla de funciones virtuales?
1) Cuando la clase contiene al menos una función virtual.
O
2) cuando la clase base inmediata contiene al menos una función virtual.
O
3) cuando cualquier clase padre en cualquier nivel de la jerarquía contiene al menos una función virtual.
Una pregunta relacionada con esto: ¿Es posible abandonar el envío dinámico en una jerarquía de C ++?
Por ejemplo, considere el siguiente ejemplo.
#include <iostream>
using namespace std;
class A {
public:
virtual void f();
};
class B: public A {
public:
void f();
};
class C: public B {
public:
void f();
};
¿Qué clases contendrán una V-Table?
Dado que B no declara f () como virtual, ¿la clase C recibe un polimorfismo dinámico?
El comportamiento se define en el capítulo 10.3, párrafo 2 de la especificación del lenguaje C ++:
Si una función miembro virtual vf se declara en una clase Base y en una clase Derivada, derivada directa o indirectamente de Base, se declara una función miembro vf con el mismo nombre y la misma lista de parámetros que Base :: vf, luego Derived :: vf también es virtual ( esté o no así declarado ) y reemplaza a Base :: vf.
A cursiva la frase relevante. Por lo tanto, si su compilador crea tablas v en el sentido habitual, todas las clases tendrán una tabla v ya que todos sus métodos f () son virtuales.
Hizo un esfuerzo para que su pregunta fuera muy clara y precisa, pero aún falta un poco de información. Probablemente sepa que en las implementaciones que usan V-Table, la tabla en sí misma es normalmente una estructura de datos independiente, almacenada fuera de los objetos polimórficos, mientras que los objetos solo almacenan un puntero implícito a la tabla. Entonces, ¿qué es lo que estás preguntando? Podría ser:
- ¿Cuándo un objeto obtiene un puntero implícito a V-Table insertado en él?
o
- ¿Cuándo se crea una V-Table dedicada e individual para un tipo dado en la jerarquía?
La respuesta a la primera pregunta es: un objeto obtiene un puntero implícito a V-Table insertado en él cuando el objeto es de tipo de clase polimórfica. El tipo de clase es polimórfico si contiene al menos una función virtual, o cualquiera de sus padres directos o indirectos es polimórfico (esta es la respuesta 3 de tu conjunto). Tenga en cuenta también que, en caso de herencia múltiple, un objeto podría (y lo hará) terminar conteniendo múltiples punteros V-Table incrustados en él.
La respuesta a la segunda pregunta podría ser la misma que la primera (opción 3), con una posible excepción. Si alguna clase polimórfica en una jerarquía de herencia única no tiene funciones virtuales propias (no hay nuevas funciones virtuales, no hay excepciones para la función virtual principal), es posible que la implementación decida no crear una V-Table individual para esta clase, sino que también se usa la V-Table de los padres para esta clase (ya que de todos modos va a ser la misma). Es decir, en este caso, tanto los objetos de tipo padre como los objetos de tipo derivado almacenarán el mismo valor en sus punteros de V-Table incrustados. Esto es, por supuesto, altamente dependiente de la implementación. Revisé GCC y MS VS 2005 y no actúan de esa manera. Ambos crean una V-Table individual para la clase derivada en esta situación, pero parece recordar haber escuchado sobre implementaciones que no lo hacen.
La respuesta es, depende''. Depende de lo que quiere decir con ''contener un vtbl'' y depende de las decisiones tomadas por el implementador del compilador en particular.
En sentido estricto, ninguna ''clase'' contiene una tabla de funciones virtuales. Algunas instancias de algunas clases contienen punteros a tablas de funciones virtuales. Sin embargo, esa es solo una posible implementación de la semántica.
En el extremo, un compilador podría hipotéticamente poner un número único en la instancia que se indexó en una estructura de datos utilizada para seleccionar la instancia de función virtual apropiada.
Si preguntas, ''¿Qué hace GCC?'' o ''¿Qué hace Visual C ++?'' entonces podrías obtener una respuesta concreta.
La respuesta de @Hassan Syed es probablemente más cercana a lo que estaba preguntando, pero es realmente importante mantener los conceptos correctos aquí.
Existe un comportamiento (envío dinámico basado en qué clase fue nueva) y hay implementación . Su pregunta utilizó la terminología de implementación, aunque sospecho que estaba buscando una respuesta de comportamiento.
La respuesta de comportamiento es la siguiente: cualquier clase que declare o herede una función virtual exhibirá un comportamiento dinámico en las llamadas a esa función. Cualquier clase que no lo haga, no lo hará.
En cuanto a la implementación, al compilador se le permite hacer lo que quiera para lograr ese resultado.
Los estándares de C ++ no imponen el uso de tablas V para crear la ilusión de clases polimórficas. La mayoría de las implementaciones de tiempo usan V-Tables, para almacenar la información adicional necesaria. En resumen, estas piezas adicionales de información están equipadas cuando tiene al menos una función virtual.
Más allá de "vtables son específicos de la implementación" (que son), si se usa vtable: habrá vtables únicos para cada una de sus clases. Aunque B :: f y C :: f no se declaran virtuales, porque existe una firma coincidente en un método virtual de una clase base ( A en su código), B :: f y C :: f son implícitamente virtuales . Debido a que cada clase tiene al menos un método virtual único ( B :: f anula las instancias A :: f para B y C :: f de manera similar para las instancias C ), necesita tres vtables.
Generalmente no debes preocuparte por tales detalles. Lo que importa es si tienes un envío virtual o no. No tiene que usar el envío virtual, especificando explícitamente a qué función llamar, pero esto generalmente es útil solo cuando se implementa un método virtual (como llamar al método de la base). Ejemplo:
struct B {
virtual void f() {}
virtual void g() {}
};
struct D : B {
virtual void f() { // would be implicitly virtual even if not declared virtual
B::f();
// do D-specific stuff
}
virtual void g() {}
};
int main() {
{
B b; b.g(); b.B::g(); // both call B::g
}
{
D d;
B& b = d;
b.g(); // calls D::g
b.B::g(); // calls B::g
b.D::g(); // not allowed
d.D::g(); // calls D::g
void (B::*p)() = &B::g;
(b.*p)(); // calls D::g
// calls through a function pointer always use virtual dispatch
// (if the pointed-to function is virtual)
}
return 0;
}
Algunas reglas concretas que pueden ayudar; pero no me cites en esto, probablemente me he perdido algunos casos de vanguardia:
- Si una clase tiene métodos virtuales o bases virtuales, incluso si se heredan, las instancias deben tener un puntero vtable.
- Si una clase declara métodos virtuales no heredados (como cuando no tiene una clase base), entonces debe tener su propia vtable.
- Si una clase tiene un conjunto diferente de métodos de reemplazo que su primera clase base, entonces debe tener su propia vtable y no puede reutilizar la base. (Los destructores comúnmente requieren esto).
- Si una clase tiene varias clases base, con la segunda base o posterior con métodos virtuales:
- Si ninguna base anterior tiene métodos virtuales y la optimización de la base vacía se aplicó a todas las bases anteriores, entonces trate esta base como la primera clase base.
- De lo contrario, la clase debe tener su propia vtable.
- Si una clase tiene clases de base virtual, debe tener su propia vtable.
Recuerde que un vtable es similar a un miembro de datos estáticos de una clase, y las instancias solo tienen punteros a estos.
También vea el artículo completo C ++: Under the Hood (marzo de 1994) de Jan Gray. ( Prueba con Google si ese enlace muere).
Ejemplo de reutilización de un vtable:
struct B {
virtual void f();
};
struct D : B {
// does not override B::f
// does not have other virtuals of its own
void g(); // still might have its own non-virtuals
int n; // and data members
};
En particular, el aviso dtor de B no es virtual (y esto probablemente sea un error en el código real ), pero en este ejemplo, las instancias de D apuntarán a la misma vtable que las instancias de B.
Responder
se crea un vtable cuando una declaración de clase contiene una función virtual. Se presenta un vtable cuando un padre, en cualquier lugar de la jerarquía, tiene una función virtual, llamémosle a este padre Y. Cualquier padre de Y NO TENDRÁ un vtable (a menos que tenga un virtual
para alguna otra función en su jerarquía).
Siga leyendo para discusión y pruebas
- explicación
Cuando especifica una función miembro como virtual, existe la posibilidad de que intente usar subclases a través de una clase base polimórficamente en tiempo de ejecución. Para mantener la garantía de rendimiento de c ++ sobre el diseño del lenguaje, ofrecieron la estrategia de implementación más liviana posible, es decir, un nivel de direccionamiento indirecto, y solo cuando una clase podría usarse polimorfamente en tiempo de ejecución, y el programador especifica esto configurando al menos una función para que sea virtual.
No incurra en el costo de vtable si evita la palabra clave virtual.
- editar: para reflejar su edición -
Solo cuando una clase base contiene una función virtual, cualquier otra subclase contiene una vtable. Los padres de dicha clase base no tienen una vtable.
En su ejemplo, las tres clases tendrán una vtable, esto se debe a que puede intentar usar las tres clases a través de un A*.
--test - GCC 4+ -
#include <iostream>
class test_base
{
public:
void x(){std::cout << "test_base" << "/n"; };
};
class test_sub : public test_base
{
public:
virtual void x(){std::cout << "test_sub" << "/n"; } ;
};
class test_subby : public test_sub
{
public:
void x() { std::cout << "test_subby" << "/n"; }
};
int main()
{
test_sub sub;
test_base base;
test_subby subby;
test_sub * psub;
test_base *pbase;
test_subby * psubby;
pbase = ⊂
pbase->x();
psub = &subby;
psub->x();
return 0;
}
salida
test_base
test_subby
test_base
no tiene una tabla virtual, por lo tanto, todo lo que se le test_base
utilizará la x()
de test_base
. test_sub
otra parte, test_sub
cambia la naturaleza de x()
y su puntero será indirecto a través de un vtable, y esto se muestra cuando se test_subby
x()
test_subby
.
Por lo tanto, un vtable solo se introduce en la jerarquía cuando se usa la palabra clave virtual. Los antepasados más antiguos no tienen un vtable, y si se produce un retroceso, estará conectado a las funciones de los antepasados.