2009-02-18 10 views
10

¿Todos los objetos de la clase virtual tienen un puntero a vtable?¿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?

+0

¿Duplicado? http://stackoverflow.com/questions/99297/at-as-deep-of-a-level-as-possible-how-are-virtual-functions-implemented – Anonymous

+0

No existe una "clase virtual" en C++. – curiousguy

Respuesta

1

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.

4

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.

+0

¿Qué pasa con Vptr, habrá 10 vptr asociados con cada objeto o, como vtable único, habrá solo un vptr? – Rndp13

0

Cada objeto de tipo polimórfico tendrá un puntero a Vtable.

Donde VTable almacenado depende del compilador.

15

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), normalmente 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 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.

+2

+1 ¿Y podría explicar por qué el puntero virtual es por objeto y no por clase? Gracias. – Viet

+1

@Viet Puede pensar en el vPtr como un arranque de la definición de tiempo de ejecución de un objeto. Solo después de configurar vPtr, el objeto puede saber cuál es su tipo real. En esta noción, hacer un vPtr por clase (estático) no tiene sentido. Pensando en esto de otra manera, si un objeto no necesita un vPtr, entonces ya debe conocer su definición de tiempo de ejecución durante el tiempo de compilación, lo que contradice que sea un objeto resuelto dinámicamente. –

0

No necesariamente

Prácticamente todos los objetos que tiene una función virtual tendrá un puntero 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 de función regulares porque siempre que se llame a la función virtual siempre resolver a la misma función exacta.

Además, si solo hay un par de funciones concretas diferentes, el compilador puede cambiar efectivamente el sitio de llamada para que use un 'si' para seleccionar la función concreta adecuada para llamar.

Por lo tanto, en casos como este la tabla v no es necesaria y los objetos pueden terminar sin tener una.

+0

Hmm. Solo he estado tratando de encontrar un compilador que elimine el puntero de la tabla v. No parece que haya actualmente. Sin embargo, el intercambio de información entre los compiladores y los vinculadores es cada vez mayor, de modo que se fusionan. Con el desarrollo continuo, esto puede suceder. –

+0

Esto podría deberse a que eliminar el vptr significaría una violación grave del ABI, y esto requeriría asegurarse de que ningún objeto de la clase en cuestión nunca se vea fuera del módulo, por solo 4 bytes de memoria, lo que incluso puede no se guarda realmente – jpalecek

+0

OTOH, simplemente no llama a los métodos a través de interrupciones de despacho virtual solo la interfaz de ese método en particular, y el compilador puede resolver esto al emitir otra versión del código con pleno despacho virtual. También ofrece una mayor ventaja, especialmente si la función puede estar en línea – jpalecek

4

intente 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"; 
} 
+1

Respuestas para VS2005: 1, 4, 4 –

+1

El dibujo de la conclusión necesaria fue 'dejado como una tarea' para el OP;) – dirkgently

+0

Estos números son típicos, aunque no obligatorios. No dice cuántos vtables existen, o en qué se gastan estos 4 bytes. – jalf

2

Un Vtable es un detalle de implementación no hay nada en la definición del lenguaje que dice 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 demás preguntas que haga dependerán del compilador/hardware, no existe una respuesta real a esas preguntas.

11

Como dijo otra persona, el Estándar C++ no ordena una tabla de método virtual, pero permite que se use una. He hecho mis pruebas utilizando gcc y el código y uno de los escenarios más simple posible:

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); 
} 

de datos son miembros Agregado para evitar que el compilador para dar la clase base un tamaño de cero (que se conoce como la clase vacía-base-optimización). Este es el diseño que eligió GCC: (imprimir utilizando -fdump de clase-jerarquía)

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) 

Al 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 he volcado el árbol generado por GCC después de algunas optimizaciones (constructor en línea, ...), con -fdump-tree-optimized. La salida está usando el lenguaje de gama media de GCC GIMPL que es de interfaces independientes, con una sangría en alguna estructura de bloques similar a 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 sólo la creación de un puntero - VPTR - que será apunte 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 el primer argumento de registro, $ 2 es return value register, $ 0 es always-0-register) después de demandar los nombres en él por la 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é pasa si queremos llamar bark:?

void doit(Base* b) { 
    b->bark(); 
} 

Gimpl código:

;; 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 GIMP L constructo que prácticamente se imprime en (se documenta en gcc/tree.def en el gcc código fuente SVN)

OBJ_TYPE_REF(<first arg>; <second arg> -> <third arg>) 

su significado: Utilice la expresión *b->_vptr.Base en el objeto b, y almacenar el frontend (C++) valor específico 0 (es el índice en el vtable). Finalmente, está pasando b como el argumento "this". ¿Llamaríamos a una función que aparece en el segundo índice en el vtable (nota, no sabemos qué variable 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 ensamblador de nuevo (cosas apilar-marco cortada):

# 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 los puntos VPTR 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 ocurre como la última declaración en nuestra función doit.

1

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, dada la 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, se necesitan cinco vtables: A, B, C, D, E y todos necesitan sus propios vtables.

A continuación, debe saber qué vtable utilizar 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 se podría generar el código de despacho virtual correcto 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. Así, en el ejemplo anterior, también se puede Disposición B como:

B's vtable; 
int a; 
int b; 

Debido vtable de B es un superconjunto de Atléticos. El vtable de B tiene entradas para A :: f y B :: g, y el vtable de A tiene entradas para A :: f.

Para completar, esta es la forma en que usted disponer 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 lo actual entradas 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, lo hace 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.

De esto se puede ver que el número de punteros vtable que tiene una clase en particular es al menos el número de clases de raíz de las que se 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.

+0

Por favor, señale qué hay de malo en la respuesta cuando la vota. ¡De lo contrario, no hay forma de que podamos mejorar el contenido! Gracias. –

Cuestiones relacionadas