Tabla de métodos virtuales

AjustarCompartirImprimirCitar
Mecanismo de apoyo al envío dinámico

En programación informática, una tabla de métodos virtuales (VMT), una tabla de funciones virtuales, una 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étodo en tiempo de ejecución).

Siempre que una clase define una función (o método) virtual, 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 llamará a la función base o se implementará 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.

Supongamos que un programa contiene tres clases en una jerarquía de herencia: una superclase, Cat, y dos subclases, HouseCat y León. La clase Cat define una función virtual llamada speak, por lo que sus subclases pueden proporcionar una implementación adecuada (por ejemplo, maullido o rugido). 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 qué implementación de la función a la que 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 dinámicamente (es decir, en tiempo de ejecución).

Implementació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.

Los estándares de C++ no exigen exactamente cómo se debe implementar el envío dinámico, pero los compiladores generalmente usan variaciones menores del mismo modelo básico.

Normalmente, el compilador crea una tabla de métodos virtuales separada 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 archivos "ocultos" código 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. Por ejemplo, g++ previamente colocó el puntero al final del objeto.

Ejemplo

Considere las siguientes declaraciones de clases en sintaxis de C++:

clase B1 {}público: virtual ~B1() {} vacío fnonvirtual() {} virtual vacío f1() {} int int_in_b1;};clase B2 {}público: virtual ~B2() {} virtual vacío f2() {} int int_in_b2;};

utilizado para derivar la siguiente clase:

clase D : público B1, público B2 {}público: vacío d() {} vacío f2() Anulación {} 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:

b2:
+0: puntero a la tabla de método virtual de B2
+4: valor de int_in_b2

tabla de método virtual de B2:
+0: B2::f2()

y el siguiente diseño de memoria para el objeto d:

d:
+0: puntero a la tabla de método virtual de D (para B1)
+4: valor de int_in_b1
+8: puntero a la tabla de método virtual de D (para B2)
+12: valor de int_in_b2
+16: valor de int_in_d

Tamaño total: 20 Bytes.

tabla de método virtual de D (para B1):
+0: B1::f1() // B1::f1() no es overridden

tabla de método virtual de D (para B2):
+0: D::f2() // B2::f2() está dominado por D::f2()
// La ubicación de B2::f2 no está en la tabla de método virtual para D

Tenga en cuenta que esas funciones no llevan la palabra clave virtual en su declaración (como fnonvirtual() y d()) no aparece generalmente en la tabla de método virtual. Existen excepciones para casos especiales que plantea el constructor por defecto.

También note los destructores virtuales en las clases base, B1 y B2. Son necesarios para garantizar delete d puede liberar la memoria no sólo para D, pero 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 mantener el ejemplo simple.

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().

Herencia múltiple y procesadores

El compilador g++ implementa la herencia múltiple de las clases B1 y B2 en la clase D usando dos tablas de métodos virtuales, una para cada base. clase. (Hay otras formas de implementar la herencia múltiple, pero esta es la más común). Esto lleva a la necesidad de "arreglar punteros", también llamados procesadores, al realizar la conversión.

Considere el siguiente código C++:

D *d = nuevo D();B1 *b1 = d;B2 *b2 = d;

Mientras tanto 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 d). Así, b2 puntos a la región d que "parece como" una instancia de B2, es decir, tiene el mismo diseño de memoria como una instancia de B2.

Invocación

Una llamada a d->f1() se maneja eliminando la referencia al vpointer d D::B1 de d, buscar la entrada f1 en la tabla de métodos virtuales y luego eliminar la referencia a 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 en d (como ocurre con muchos compiladores), esto se reduce a siguiente pseudo-C++:

()*(()*d[0])d)

Donde *d se refiere a la tabla de métodos virtuales de D y [0] se refiere al primer método de la tabla de métodos virtuales. El parámetro d se convierte en el parámetro "this" puntero al objeto.

Herencia múltiple

En el caso más general, llamar a B1::f1() o D::f2() es más complicado:

()*()*()d[0]/*punto a la tabla de método virtual de D (para B1)*/[0])d) * Llamada d- título 1() */()*()*()d[8]/*punto a la tabla de método virtual de D (para B2)*/[0])d+8) * Llamada d-jóf2()*

La llamada a d->f1() pasa un puntero B1 como parámetro. La llamada a d->f2() pasa un puntero B2 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 simple:

()*B1::fnonvirtual)d)

Eficiencia

Una llamada virtual requiere al menos una dereferencia indexada extra y a veces una adición "fixup", en comparación con una llamada no virtual, que es simplemente un salto a un puntero compilado. Por lo tanto, llamar funciones virtuales es inherentemente más lento que llamar funciones no virtuales. Un experimento realizado en 1996 indica que aproximadamente el 6–13% del tiempo de ejecución se gasta simplemente despachando a la función correcta, aunque la sobrecarga puede ser tan alta como el 50%. El costo de las funciones virtuales puede no ser tan alto en moderno CPU arquitecturas debido a caches mucho más grandes y mejor predicción de rama.

Además, en entornos donde la compilación JIT no está en uso, las llamadas de función virtual generalmente no pueden estar inlineadas. En ciertos casos puede ser posible que el compilador realice un proceso conocido como devirtualización en el que, por ejemplo, la búsqueda y llamada indirecta se sustituyen por una ejecución condicional de cada cuerpo inlinedo, 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, 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 un D en este punto, y D no anula f1. O el compilador (u optimizador) puede detectar que no hay subclases de B1 en 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 el comando ' esta reparación del puntero).

Comparación con alternativas

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.

Sin embargo, las tablas de métodos virtuales solo permiten el envío único en el menú especial "this" parámetro, 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 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 tipeo (como Smalltalk, Python o JavaScript).

Los lenguajes que proporcionan una o ambas características a menudo se ejecutan 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, nombres de métodos de internación/tokenización, búsquedas en caché, compilación justo a tiempo).

Contenido relacionado

Más resultados...
Tamaño del texto: