Tabla de métodos virtuales

Mecanismo para apoyar el despacho dinámico

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

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

Ejemplo

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 virtualen 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 B1y B2. Son necesarios para garantizar delete dque se pueda liberar memoria no solo para D, sino también para B1y B2, si des un puntero o una referencia a los tipos B1o 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 Dse implementa duplicando la tabla de métodos virtuales de B2y 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 B1y B2en clase Dutilizando 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 dy b1apuntarán a la misma ubicación de memoria después de la ejecución de este código, b2apuntarán a la ubicación d+8(ocho bytes más allá de la ubicación de memoria de d). Por lo tanto, b2apunta a la región dentro dde la cual "parece" una instancia de B2, es decir, tiene la misma distribución de memoria que una instancia de B2. [ aclaración necesaria ]

Invocación

Una llamada a d->f1()se maneja desreferenciando el puntero dvirtual de D::B1, buscando la f1entrada 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 *dhace referencia a la tabla de métodos virtuales de Dy [0]hace referencia al primer método de la tabla de métodos virtuales. El parámetro dse 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 B1puntero como parámetro. La llamada a d->f2()pasa un B2puntero como parámetro. Esta segunda llamada requiere una corrección para producir el puntero correcto. La ubicación de B2::f2no 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 )

Eficiencia

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 f1anterior puede no requerir una búsqueda en la tabla porque el compilador puede ser capaz de decir que dsolo puede contener a Den este punto, y Dno anula f1. O el compilador (u optimizador) puede ser capaz de detectar que no hay subclases de en B1ninguna parte del programa que anulen f1. La llamada a B1::f1o B2::f2probablemente 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').

Comparación con alternativas

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

Véase también

Notas

  1. ^ El argumento de G++ -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_hierarchypara volcar la jerarquía de clases y el diseño de la tabla de funciones virtuales.
  2. ^ "C++: por qué hay dos destructores virtuales en la tabla virtual y dónde está la dirección de la función no virtual (gcc4.6.3)".

Referencias

  1. ^ ab Zendra, Olivier; Colnet, Dominique; Collin, Suzanne (1997). Despacho dinámico eficiente sin tablas de funciones virtuales: el compilador SmallEiffel - 12.ª Conferencia anual de ACM SIGPLAN sobre sistemas, lenguajes y aplicaciones de programación orientada a objetos (OOPSLA'97), ACM SIGPLAN, octubre de 1997, Atlanta, Estados Unidos. págs.125-141. inria-00565627. Centro de Investigación en Informática de Nancy Campus Scientifique, Bâtiment LORIA. pag. 16.
  2. ^ Ellis y Stroustrup 1990, págs. 227-232
  3. ^ Danny Kalev. "Guía de referencia de C++: El modelo de objetos II". 2003. Títulos "Herencia y polimorfismo" y "Herencia múltiple".
  4. ^ "Problemas cerrados de C++ ABI". Archivado desde el original el 25 de julio de 2011 . Consultado el 17 de junio de 2011 .{{cite web}}: CS1 maint: bot: estado de URL original desconocido ( enlace )
  5. ^ Driesen, Karel y Hölzle, Urs, "El costo directo de las llamadas a funciones virtuales en C++", OOPSLA 1996
  6. ^ Zendra, Olivier y Driesen, Karel, "Estructuras de control para pruebas de estrés para despacho dinámico en Java", págs. 105-118, Actas del 2.º Simposio de tecnología e investigación de máquinas virtuales Java de USENIX, 2002 (JVM '02)
Obtenido de "https://es.wikipedia.org/w/index.php?title=Tabla_de_métodos_virtuales&oldid=1220366429"