En programación informática , una tabla de métodos virtuales ( VMT ), tabla de funciones virtuales , tabla de llamadas virtuales , 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 adecuadas, porque en tiempo de compilación puede 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.
Existen muchas formas diferentes de implementar este tipo de distribución dinámica, 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 utilizar este enfoque, porque permite que los objetos utilicen una implementación diferente simplemente utilizando 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 speak , por lo que sus subclases pueden proporcionar una implementación apropiada (por ejemplo, meow o roar ). Cuando el programa llama a la función speak en una referencia 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 a él ( Cat ). La clase generalmente no se puede determinar de forma estática (es decir, en tiempo de compilación ), por lo que el compilador tampoco puede decidir qué función llamar en ese momento. La llamada se debe enviar 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 enlazados 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 el tipo (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 el tipo. Por lo tanto, obtener la dirección del método desde un desplazamiento determinado en una tabla de métodos virtuales obtendrá el método correspondiente a la clase real del objeto. [2]
Los estándares de C++ no establecen exactamente cómo debe implementarse el despacho dinámico, pero los compiladores generalmente utilizan 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. Por lo tanto, el compilador también debe generar código "oculto" en los constructores de cada clase para inicializar el puntero de 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 colocan como el primero; el código fuente portable funciona de cualquier manera. [3] Por ejemplo, g++ anteriormente colocaba el puntero al final del objeto. [4]
Considere las siguientes declaraciones de clase en sintaxis C++ :
clase B1 { público : virtual ~ B1 () {} void fnonvirtual () {} void virtual f1 () {} int int_in_b1 ; }; clase B2 { público : virtual ~ B2 () {} virtual void f2 () {} int int_in_b2 ; };
se utiliza 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 reemplazado por D::f2() // La ubicación de B2::f2 no está en la tabla de métodos virtuales para D
Tenga en cuenta que las funciones que no llevan la palabra clave virtual
en su declaración (como fnonvirtual()
and d()
) generalmente no aparecen en la tabla de métodos virtuales. Existen excepciones para casos especiales como los planteados por el constructor predeterminado .
Tenga en cuenta también los destructores virtuales en las clases base B1
y B2
. Son necesarios para garantizar delete d
que se pueda liberar memoria no solo para D
, sino también para B1
y B2
, si d
es un puntero o una referencia a los tipos B1
o B2
. Se excluyeron de los diseños de memoria para mantener el ejemplo simple. [nb 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()
con 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 "correcciones de punteros", también llamadas thunks , al realizar conversiones .
Considere el siguiente código C++:
D * d = nuevo D (); B1 * b1 = d ; B2 * b2 = d ;
Mientras que d
y b1
apuntarán a la misma ubicación de memoria después de la ejecución de este código, b2
apuntarán 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 la misma distribución de memoria que una instancia de B2
. [ aclaración necesaria ]
Una llamada a d->f1()
se maneja desreferenciando el puntero d
virtual de D::B1
, buscando la f1
entrada en la tabla de métodos virtuales y luego desreferenciando ese puntero para llamar al código.
Herencia única
En el caso de herencia simple (o en un lenguaje con solo herencia simple), si el vpointer es siempre el primer elemento d
(como sucede con muchos compiladores), esto se reduce al siguiente pseudo-C++:
( * (( * d )[ 0 ]))( d )
Donde *d
hace referencia a la tabla de métodos virtuales de D
y [0]
hace referencia al primer método de la tabla de métodos virtuales. El parámetro d
se convierte en el puntero "this
" 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 correcció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 :: fno virtual )( d )
Una llamada virtual requiere al menos una desreferencia indexada adicional y, a veces, la adición de una "correcció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 el 6-13% del tiempo de ejecución se gasta simplemente en enviar a la función correcta, aunque la sobrecarga puede llegar al 50%. [5] El costo de las funciones virtuales puede no ser 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 en los que no se utiliza la compilación JIT , las llamadas a funciones virtuales normalmente no se pueden incluir en línea . 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 incluido en línea, pero dichas optimizaciones no son comunes.
Para evitar esta sobrecarga, los compiladores generalmente evitan el uso de tablas de métodos virtuales siempre que la llamada se pueda resolver en tiempo de compilación .
Por lo tanto, la llamada a f1
anterior puede no requerir una búsqueda en la tabla porque el compilador puede ser capaz de decir que d
solo puede contener a D
en este punto, y D
no anula f1
. O el compilador (u optimizador) puede ser capaz de detectar que no hay subclases de 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 'this').
La tabla de métodos virtuales es generalmente una buena compensación de rendimiento para lograr un despacho dinámico, pero existen alternativas, como el despacho de árbol binario, con mayor rendimiento en algunos casos típicos, pero con diferentes compensaciones. [1] [6]
Sin embargo, las tablas de métodos virtuales solo permiten un envío único en el parámetro especial "this", a diferencia del 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 funcionan únicamente si el despacho 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 tipado de pato (como Smalltalk , Python o JavaScript ).
Los lenguajes que ofrecen una o ambas de estas características suelen despachar mediante la búsqueda de una cadena en una tabla hash o algún otro método equivalente. Hay una variedad de técnicas para hacer que esto sea más rápido (por ejemplo, la internación /tokenización de nombres de métodos, el almacenamiento en caché de búsquedas, la compilación en tiempo real ).
-fdump-class-hierarchy
(a partir de la versión 8: -fdump-lang-class
) se puede utilizar para volcar tablas de métodos virtuales para inspección manual. Para el compilador AIX VisualAge XlC, se utiliza -qdump_class_hierarchy
para volcar la jerarquía de clases y el diseño de la tabla de funciones virtuales.{{cite web}}
: CS1 maint: bot: estado de URL original desconocido ( enlace )