puro - que es un objeto en c++
¿Todos los objetos de la clase virtual tienen un puntero a vtable? (9)
Cada objeto de tipo polimórfico tendrá un puntero a Vtable.
Donde VTable almacenado depende del compilador.
¿Todos los objetos de la clase virtual tienen un puntero a vtable?
¿O solo lo tiene el objeto de la clase base con función virtual?
¿Dónde se almacenó el vtable? sección de código o sección de datos del proceso?
Como dijo otra persona, el Estándar C ++ no ordena una tabla de métodos virtuales, pero permite que se use una. He hecho mis pruebas usando gcc y este código y uno de los escenarios más simples posibles:
class Base {
public:
virtual void bark() { }
int dont_do_ebo;
};
class Derived1 : public Base {
public:
virtual void bark() { }
int dont_do_ebo;
};
class Derived2 : public Base {
public:
virtual void smile() { }
int dont_do_ebo;
};
void use(Base* );
int main() {
Base * b = new Derived1;
use(b);
Base * b1 = new Derived2;
use(b1);
}
Se agregaron miembros de datos para evitar que el compilador le proporcione a la clase base un tamaño de cero (se lo conoce como la clase vacía-base-optimización). Este es el diseño que GCC eligió: (imprimir utilizando -fdump-class-hierarchy)
Vtable for Base
Base::_ZTV4Base: 3u entries
0 (int (*)(...))0
4 (int (*)(...))(& _ZTI4Base)
8 Base::bark
Class Base
size=8 align=4
base size=8 base align=4
Base (0xb7b578e8) 0
vptr=((& Base::_ZTV4Base) + 8u)
Vtable for Derived1
Derived1::_ZTV8Derived1: 3u entries
0 (int (*)(...))0
4 (int (*)(...))(& _ZTI8Derived1)
8 Derived1::bark
Class Derived1
size=12 align=4
base size=12 base align=4
Derived1 (0xb7ad6400) 0
vptr=((& Derived1::_ZTV8Derived1) + 8u)
Base (0xb7b57ac8) 0
primary-for Derived1 (0xb7ad6400)
Vtable for Derived2
Derived2::_ZTV8Derived2: 4u entries
0 (int (*)(...))0
4 (int (*)(...))(& _ZTI8Derived2)
8 Base::bark
12 Derived2::smile
Class Derived2
size=12 align=4
base size=12 base align=4
Derived2 (0xb7ad64c0) 0
vptr=((& Derived2::_ZTV8Derived2) + 8u)
Base (0xb7b57c30) 0
primary-for Derived2 (0xb7ad64c0)
Como puede ver, cada clase tiene un vtable. Las primeras dos entradas son especiales. El segundo apunta a los datos RTTI de la clase. El primero, lo sabía, pero lo olvidé. Tiene algún uso en casos más complicados. Bueno, como muestra el diseño, si tiene un objeto de clase Derived1, entonces el vptr (v-table-pointer) apuntará a la tabla v de la clase Derived1, por supuesto, que tiene exactamente una entrada para su función ladrar apuntando a La versión de Derived1. El vptr de Derived2 apunta al vtable de Derived2, que tiene dos entradas. El otro es el nuevo método que se agrega, sonríe. Repite la entrada para Base :: bark, que apuntará a la versión de base de la función, por supuesto, porque es la versión más derivada de la misma.
También abandoné el árbol generado por GCC después de realizar algunas optimizaciones (constructor en línea, ...), con -fdump-tree-optimized. La salida está utilizando el lenguaje GIMPL
de middle-end de GIMPL
que es independiente del front-end, con sangría en una estructura de bloque tipo C:
;; Function virtual void Base::bark() (_ZN4Base4barkEv)
virtual void Base::bark() (this)
{
<bb 2>:
return;
}
;; Function virtual void Derived1::bark() (_ZN8Derived14barkEv)
virtual void Derived1::bark() (this)
{
<bb 2>:
return;
}
;; Function virtual void Derived2::smile() (_ZN8Derived25smileEv)
virtual void Derived2::smile() (this)
{
<bb 2>:
return;
}
;; Function int main() (main)
int main() ()
{
void * D.1757;
struct Derived2 * D.1734;
void * D.1756;
struct Derived1 * D.1693;
<bb 2>:
D.1756 = operator new (12);
D.1693 = (struct Derived1 *) D.1756;
D.1693->D.1671._vptr.Base = &_ZTV8Derived1[2];
use (&D.1693->D.1671);
D.1757 = operator new (12);
D.1734 = (struct Derived2 *) D.1757;
D.1734->D.1682._vptr.Base = &_ZTV8Derived2[2];
use (&D.1734->D.1682);
return 0;
}
Como podemos ver muy bien, es solo establecer un puntero - el vptr - que apuntará al vtable apropiado que hemos visto antes al crear el objeto. También he abandonado el código de ensamblador para la creación de Derived1 y call to use ($ 4 es primer argumento de registro, $ 2 es return value register, $ 0 es always-0-register) después de exigir los nombres en él por c++filt
herramienta c++filt
:)
# 1st arg: 12byte
add $4, $0, 12
# allocate 12byte
jal operator new(unsigned long)
# get ptr to first function in the vtable of Derived1
add $3, $0, vtable for Derived1+8
# store that pointer at offset 0x0 of the object (vptr)
stw $3, $2, 0
# 1st arg is the address of the object
add $4, $0, $2
jal use(Base*)
¿Qué sucede si queremos llamar a bark
?
void doit(Base* b) {
b->bark();
}
Código GIMPL:
;; Function void doit(Base*) (_Z4doitP4Base)
void doit(Base*) (b)
{
<bb 2>:
OBJ_TYPE_REF(*b->_vptr.Base;b->0) (b) [tail call];
return;
}
OBJ_TYPE_REF
es una construcción GIMPL que está bastante impresa (está documentada en gcc/tree.def
en el código fuente gcc SVN)
OBJ_TYPE_REF(<first arg>; <second arg> -> <third arg>)
Es importante: utilice la expresión *b->_vptr.Base
en el objeto b
, y almacene el valor específico 0
la interfaz (c ++) (es el índice en la tabla variable). Finalmente, pasa b
como el argumento "este". ¿Llamaríamos a una función que aparece en el segundo índice en el vtable (nota, no sabemos qué tipo de tabla de qué tipo!), El GIMPL se vería así:
OBJ_TYPE_REF(*(b->_vptr.Base + 4);b->1) (b) [tail call];
Por supuesto, aquí el código de ensamblaje nuevamente (material de marco de pila cortado):
# load vptr into register $2
# (remember $4 is the address of the object,
# doit''s first arg)
ldw $2, $4, 0
# load whatever is stored there into register $2
ldw $2, $2, 0
# jump to that address. note that "this" is passed by $4
jalr $2
Recuerde que el vptr señala exactamente en la primera función. (Antes de esa entrada, se almacenaba la ranura RTTI). Entonces, lo que aparece en esa ranura se llama. También marca la llamada como tail-call, porque sucede como la última declaración en nuestra función doit
.
Prueba esto en casa:
#include <iostream>
struct non_virtual {};
struct has_virtual { virtual void nop() {} };
struct has_virtual_d : public has_virtual { virtual void nop() {} };
int main(int argc, char* argv[])
{
std::cout << sizeof non_virtual << "/n"
<< sizeof has_virtual << "/n"
<< sizeof has_virtual_d << "/n";
}
Todas las clases con un método virtual tendrán un único vtable compartido por todos los objetos de la clase.
Cada instancia de objeto tendrá un puntero a ese vtable (así es como se encuentra el vtable), típicamente llamado vptr. El compilador genera código de forma implícita para inicializar el vptr en el constructor.
Tenga en cuenta que nada de esto es exigido por el lenguaje C ++: una implementación puede manejar el despacho virtual de otra manera si así lo desea. Sin embargo, esta es la implementación que usa cada compilador con el que estoy familiarizado. El libro de Stan Lippman, "Dentro del modelo de objetos C ++" describe cómo funciona esto muy bien.
Todas las clases virtuales generalmente tienen un vtable, pero no es requerido por el estándar de C ++ y el método de almacenamiento depende del compilador.
Una tabla V es un detalle de implementación. No hay nada en la definición del lenguaje que diga que existe. De hecho, he leído sobre métodos alternativos para implementar funciones virtuales.
PERO: todos los compiladores comunes (es decir, los que conozco) usan VTabels.
Entonces sí. Cualquier clase que tenga un método virtual o se derive de una clase (directa o indirectamente) que tenga un método virtual tendrá objetos con un puntero a una tabla VTable.
Todas las otras preguntas que haga dependerán del compilador / hardware; no hay una respuesta real a esas preguntas.
Vtable es una instancia por clase, es decir, si tengo 10 objetos de una clase que tiene un método virtual, solo hay un vtable que se comparte entre los 10 objetos.
Todos los 10 objetos en este caso apuntan a la misma vtable.
No necesariamente
Casi todos los objetos que tienen una función virtual tendrán un puntero de tabla v. No es necesario que haya un puntero v-table para cada clase que tenga una función virtual de la que se deriva el objeto.
Sin embargo, en algunos casos, los nuevos compiladores que analizan el código de manera suficiente pueden eliminar las tablas v.
Por ejemplo, en un caso simple: si solo tiene una implementación concreta de una clase base abstracta, el compilador sabe que puede cambiar las llamadas virtuales para que sean llamadas a funciones regulares porque siempre que se llame a la función virtual siempre se resolverá de la forma exacta misma función.
Además, si solo hay un par de funciones concretas diferentes, el compilador puede cambiar efectivamente el sitio de llamadas para que use un ''si'' para seleccionar la función concreta adecuada para llamar.
Entonces, en casos como este, la tabla v no es necesaria y los objetos pueden terminar sin tener una.
Para responder a la pregunta sobre qué objetos (instancias a partir de ahora) tienen tablas virtuales y dónde, es útil pensar cuándo necesita un puntero vtable.
Para cualquier jerarquía de herencia, necesita un vtable para cada conjunto de funciones virtuales definidas por una clase particular en esa jerarquía. En otras palabras, dado lo siguiente:
class A { virtual void f(); int a; };
class B: public A { virtual void f(); virtual void g(); int b; };
class C: public B { virtual void f(); virtual void g(); virtual void h(); int c; };
class D: public A { virtual void f(); int d; };
class E: public B { virtual void f(); int e; };
Como resultado, necesita cinco vtables: A, B, C, D y E necesitan sus propios vtables.
A continuación, debe saber qué tabla de acceso usar dado un puntero o referencia a una clase en particular. Por ejemplo, dado un puntero a A, necesita saber lo suficiente sobre el diseño de A como para que pueda obtener un vtable que le indique dónde enviar A :: f (). Dado un puntero a B, necesita saber lo suficiente sobre el diseño de B para enviar B :: f () y B :: g (). Y así sucesivamente y así sucesivamente.
Una implementación posible podría poner un puntero vtable como el primer miembro de cualquier clase. Eso significaría que el diseño de una instancia de A sería:
A''s vtable;
int a;
Y una instancia de B sería:
A''s vtable;
int a;
B''s vtable;
int b;
Y podría generar código de despacho virtual correcto a partir de este diseño.
También puede optimizar el diseño combinando punteros vtable de vtables que tienen el mismo diseño o si uno es un subconjunto del otro. Entonces, en el ejemplo anterior, también podrías diseñar B como:
B''s vtable;
int a;
int b;
Porque B''s vtable es un superconjunto de A''s. El vtable de B tiene entradas para A :: f y B :: g, y el vtable de A tiene entradas para A :: f.
Para completar, así es como diseñaría todos los vtables que hemos visto hasta ahora:
A''s vtable: A::f
B''s vtable: A::f, B::g
C''s vtable: A::f, B::g, C::h
D''s vtable: A::f
E''s vtable: A::f, B::g
Y las entradas reales serían:
A''s vtable: A::f
B''s vtable: B::f, B::g
C''s vtable: C::f, C::g, C::h
D''s vtable: D::f
E''s vtable: E::f, B::g
Para la herencia múltiple, haces el mismo análisis:
class A { virtual void f(); int a; };
class B { virtual void g(); int b; };
class C: public A, public B { virtual void f(); virtual void g(); int c; };
Y los diseños resultantes serían:
A:
A''s vtable;
int a;
B:
B''s vtable;
int b;
C:
C''s A vtable;
int a;
C''s B vtable;
int b;
int c;
Necesita un puntero a un vtable compatible con A y un puntero a un vtable compatible con B porque una referencia a C se puede convertir a una referencia de A o B y necesita enviar funciones virtuales a C.
A partir de esto, puede ver que el número de punteros vtable que tiene una clase en particular es, al menos, el número de clases raíz del que deriva (ya sea directamente o debido a una superclase). Una clase raíz es una clase que tiene un vtable que no hereda de una clase que también tiene un vtable.
La herencia virtual arroja otro poco de indirección en la mezcla, pero puede usar la misma métrica para determinar el número de punteros vtable.