En programación de computadoras , una tabla de métodos virtuales ( VMT ), una tabla de funciones virtuales , una tabla de llamadas virtuales , una tabla de despacho , vtable o vftable es un mecanismo utilizado en un lenguaje de programación para admitir el despacho dinámico (o enlace de métodos en tiempo de ejecución ).
Siempre que una clase define una función virtual (o método ), la mayoría de los compiladores agregan una variable miembro oculta a la clase que apunta a una matriz de punteros a funciones (virtuales) llamada tabla de métodos virtuales. Estos punteros se utilizan en tiempo de ejecución para invocar las implementaciones de funciones apropiadas, porque en el momento de la compilación es posible que aún no se sepa si se va a llamar a la función base o a una derivada implementada por una clase que hereda de la clase base.
Hay muchas formas diferentes de implementar dicho despacho dinámico, pero el uso de tablas de métodos virtuales es especialmente común entre C++ y lenguajes relacionados (como D y C# ). Los lenguajes que separan la interfaz programática de los objetos de la implementación, como Visual Basic y Delphi , también tienden a usar este enfoque, porque permite que los objetos usen una implementación diferente simplemente usando un conjunto diferente de punteros de método. El método permite la creación de bibliotecas externas, donde otras técnicas quizás no lo hagan. [1]
Supongamos que un programa contiene tres clases en una jerarquía de herencia: una superclase , Cat , y dos subclases , HouseCat y Lion . La clase Cat define una función virtual llamada hablar , por lo que sus subclases pueden proporcionar una implementación adecuada (por ejemplo, maullido o rugido ). Cuando el programa llama a la función hablar en una referencia de Cat (que puede hacer referencia a una instancia de Cat o una instancia de HouseCat o Lion ), el código debe poder determinar a qué implementación de la función se debe enviar la llamada . Esto depende de la clase real del objeto, no de la clase de la referencia al mismo ( Cat ). La clase generalmente no se puede determinar estáticamente (es decir, en tiempo de compilación ), por lo que el compilador tampoco puede decidir qué función llamar en ese momento. En su lugar , la llamada debe enviarse a la función correcta de forma dinámica (es decir, en tiempo de ejecución ).
La tabla de métodos virtuales de un objeto contendrá las direcciones de los métodos vinculados dinámicamente del objeto. Las llamadas a métodos se realizan obteniendo la dirección del método de la tabla de métodos virtuales del objeto. La tabla de métodos virtuales es la misma para todos los objetos que pertenecen a la misma clase y, por lo tanto, normalmente se comparte entre ellos. Los objetos que pertenecen a clases compatibles con tipos (por ejemplo, hermanos en una jerarquía de herencia) tendrán tablas de métodos virtuales con el mismo diseño: la dirección de un método determinado aparecerá en el mismo desplazamiento para todas las clases compatibles con tipos. Por lo tanto, al buscar la dirección del método desde un desplazamiento determinado en una tabla de métodos virtuales, se obtendrá el método correspondiente a la clase real del objeto. [2]
Los estándares de C++ no exigen exactamente cómo se debe implementar el despacho dinámico, pero los compiladores generalmente usan variaciones menores del mismo modelo básico.
Normalmente, el compilador crea una tabla de métodos virtuales independiente para cada clase. Cuando se crea un objeto, se agrega un puntero a esta tabla, llamado puntero de tabla virtual , vpointer o VPTR , como miembro oculto de este objeto. Como tal, el compilador también debe generar código "oculto" en los constructores de cada clase para inicializar el puntero de la tabla virtual de un nuevo objeto a la dirección de la tabla de métodos virtuales de su clase.
Muchos compiladores colocan el puntero de la tabla virtual como el último miembro del objeto; otros compiladores lo sitúan como el primero; El código fuente portátil funciona de cualquier manera. [3] Por ejemplo, g++ previamente colocó el puntero al final del objeto. [4]
Considere las siguientes declaraciones de clase en sintaxis de C++ :
clase B1 { público : virtual ~ B1 () {} void fnonvirtual () {} virtual void f1 () {} int int_in_b1 ; }; clase B2 { público : virtual ~ B2 () {} virtual void f2 () {} int int_in_b2 ; };
utilizado para derivar la siguiente clase:
clase D : público B1 , público B2 { público : void d () {} void f2 () anular {} int int_in_d ; };
y el siguiente fragmento de código C++:
B2 * b2 = nuevo B2 (); D * d = nuevo D ();
g++ 3.4.6 de GCC produce el siguiente diseño de memoria de 32 bits para el objeto b2
: [nb 1]
b2: +0: puntero a la tabla de métodos virtuales de B2 +4: valor de int_in_b2tabla de métodos virtuales de B2: +0: B2::f2()
y el siguiente diseño de memoria para el objeto d
:
d: +0: puntero a la tabla de métodos virtuales de D (para B1) +4: valor de int_in_b1 +8: puntero a la tabla de métodos virtuales de D (para B2) +12: valor de int_in_b2 +16: valor de int_in_dTamaño total: 20 Bytes.tabla de métodos virtuales de D (para B1): +0: B1::f1() // B1::f1() no se anulatabla de métodos virtuales de D (para B2): +0: D::f2() // B2::f2() es anulado por D::f2() // La ubicación de B2::f2 no está en la tabla de métodos virtuales para D
Tenga en cuenta que aquellas funciones que no llevan la palabra clave virtual
en su declaración (como fnonvirtual()
y d()
) generalmente no aparecen en la tabla de métodos virtuales. Hay excepciones para casos especiales según lo plantea el constructor predeterminado .
Tenga en cuenta también los destructores virtuales en las clases base B1
y B2
. Son necesarios para garantizar que delete d
se pueda liberar memoria no solo para D
, sino también para B1
y B2
, si d
es un puntero o referencia a los tipos B1
o B2
. Fueron excluidos de los diseños de memoria para simplificar el ejemplo. [nota 2]
La anulación del método f2()
en la clase D
se implementa duplicando la tabla de métodos virtuales de B2
y reemplazando el puntero a B2::f2()
por un puntero a D::f2()
.
El compilador g++ implementa la herencia múltiple de las clases B1
y B2
en clase D
utilizando dos tablas de métodos virtuales, una para cada clase base. (Existen otras formas de implementar la herencia múltiple, pero esta es la más común). Esto lleva a la necesidad de "reparaciones de puntero", también llamadas procesadores , al realizar la conversión .
Considere el siguiente código C++:
D * d = nuevo D (); B1 * b1 = d ; B2 * b2 = d ;
Mientras que d
y b1
apuntará a la misma ubicación de memoria después de la ejecución de este código, b2
apuntará a la ubicación d+8
(ocho bytes más allá de la ubicación de memoria de d
). Por lo tanto, b2
apunta a la región dentro d
de la cual "parece" una instancia de B2
, es decir, tiene el mismo diseño de memoria que una instancia de B2
. [ se necesita aclaración ]
Una llamada a d->f1()
se maneja eliminando la referencia d
del D::B1
vpointer de, buscando la f1
entrada en la tabla de métodos virtuales y luego eliminando la referencia de ese puntero para llamar al código.
herencia única
En el caso de herencia única (o en un lenguaje con herencia única), si el vpointer es siempre el primer elemento d
(como ocurre con muchos compiladores), esto se reduce al siguiente pseudo-C++:
( * (( * re )[ 0 ]))( re )
Donde *d
se refiere a la tabla de métodos virtuales de D
y [0]
se refiere al primer método en la tabla de métodos virtuales. El parámetro d
se convierte en el " this
" puntero al objeto.
herencia múltiple
En el caso más general, llamar B1::f1()
a o D::f2()
es más complicado:
( * ( * ( d [ 0 ] /*puntero a la tabla de métodos virtuales de D (para B1)*/ )[ 0 ]))( d ) /* Llamar a d->f1() */ ( * ( * ( d [ 8 ] /*puntero a la tabla de métodos virtuales de D (para B2)*/ )[ 0 ]))( d + 8 ) /* Llamar a d->f2() */
La llamada a d->f1()
pasa un B1
puntero como parámetro. La llamada a d->f2()
pasa un B2
puntero como parámetro. Esta segunda llamada requiere una reparación para producir el puntero correcto. La ubicación de B2::f2
no está en la tabla de métodos virtuales para D
.
En comparación, una llamada a d->fnonvirtual()
es mucho más sencilla:
( * B1 :: fnovirtual )( d )
Una llamada virtual requiere al menos una desreferencia indexada adicional y, a veces, una adición de "reparación", en comparación con una llamada no virtual, que es simplemente un salto a un puntero compilado. Por lo tanto, llamar a funciones virtuales es inherentemente más lento que llamar a funciones no virtuales. Un experimento realizado en 1996 indica que aproximadamente entre el 6% y el 13% del tiempo de ejecución se dedica simplemente a despachar a la función correcta, aunque la sobrecarga puede llegar al 50%. [5] Es posible que el costo de las funciones virtuales no sea tan alto en las arquitecturas de CPU modernas debido a cachés mucho más grandes y una mejor predicción de bifurcaciones .
Además, en entornos donde no se utiliza la compilación JIT , las llamadas a funciones virtuales generalmente no se pueden insertar . En ciertos casos, es posible que el compilador realice un proceso conocido como desvirtualización en el que, por ejemplo, la búsqueda y la llamada indirecta se reemplazan con una ejecución condicional de cada cuerpo insertado, pero tales optimizaciones no son comunes.
Para evitar esta sobrecarga, los compiladores generalmente evitan el uso de tablas de métodos virtuales siempre que la llamada pueda resolverse en tiempo de compilación .
Por lo tanto, es posible que la llamada f1
anterior no requiera una búsqueda en la tabla porque el compilador puede saber que d
solo puede contener a D
en este punto y D
no anula f1
. O el compilador (u optimizador) puede detectar que no hay subclases en B1
ninguna parte del programa que anulen f1
. La llamada a B1::f1
o B2::f2
probablemente no requerirá una búsqueda en la tabla porque la implementación se especifica explícitamente (aunque todavía requiere la corrección del puntero 'este').
La tabla de métodos virtuales es generalmente una buena compensación de rendimiento para lograr el despacho dinámico, pero existen alternativas, como el envío de árbol binario, con mayor rendimiento en algunos casos típicos, pero con diferentes compensaciones. [dieciséis ]
Sin embargo, las tablas de métodos virtuales solo permiten el envío único en el parámetro especial "este", en contraste con el envío múltiple (como en CLOS , Dylan o Julia ), donde los tipos de todos los parámetros se pueden tener en cuenta en el envío.
Las tablas de métodos virtuales también solo funcionan si el envío está restringido a un conjunto conocido de métodos, por lo que se pueden colocar en una matriz simple creada en tiempo de compilación, a diferencia de los lenguajes de escritura pato (como Smalltalk , Python o JavaScript ).
Los lenguajes que proporcionan una o ambas características a menudo se envían buscando una cadena en una tabla hash o algún otro método equivalente. Existe una variedad de técnicas para hacer esto más rápido (por ejemplo, internación /tokenización de nombres de métodos, búsquedas en caché, compilación justo a tiempo ).
-fdump-class-hierarchy
(a partir de la versión 8:) -fdump-lang-class
se puede utilizar para volcar tablas de métodos virtuales para su inspección manual. Para el compilador AIX VisualAge XlC, utilícelo -qdump_class_hierarchy
para volcar la jerarquía de clases y el diseño de la tabla de funciones virtuales.{{cite web}}
: Mantenimiento CS1: bot: estado de la URL original desconocido ( enlace )