stringtranslate.com

tabla de métodos virtuales

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

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. [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]

Ejemplo

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 virtualen 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 B1y B2. Son necesarios para garantizar que delete dse pueda liberar memoria no solo para D, sino también para B1y B2, si des un puntero o referencia a los tipos B1o 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 Dse implementa duplicando la tabla de métodos virtuales de B2y reemplazando el puntero a B2::f2()por 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 "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 dy b1apuntará a la misma ubicación de memoria después de la ejecución de este código, b2apuntará 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 el mismo diseño de memoria que una instancia de B2. [ se necesita aclaración ]

Invocación

Una llamada a d->f1()se maneja eliminando la referencia ddel D::B1vpointer de, buscando la f1entrada 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 *dse refiere a la tabla de métodos virtuales de Dy [0]se refiere al primer método en la tabla de métodos virtuales. El parámetro dse 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 B1puntero como parámetro. La llamada a d->f2()pasa un B2puntero como parámetro. Esta segunda llamada requiere una reparació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 :: fnovirtual )( d )

Eficiencia

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 f1anterior no requiera una búsqueda en la tabla porque el compilador puede saber que dsolo puede contener a Den este punto y Dno anula f1. O el compilador (u optimizador) puede detectar que no hay subclases 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 'este').

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

Ver también

Notas

  1. ^ El argumento de G++ -fdump-class-hierarchy(a partir de la versión 8:) -fdump-lang-classse puede utilizar para volcar tablas de métodos virtuales para su inspección manual. Para el compilador AIX VisualAge XlC, utilícelo -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. dieciséis.
  2. ^ Ellis y Stroustrup 1990, págs. 227-232
  3. ^ Danny Kalev. "Guía de referencia de C++: el modelo de objetos II". 2003. Epígrafes “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}}: Mantenimiento CS1: bot: estado de la 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 de pruebas de estrés para el envío dinámico en Java", págs. 105-118, Actas del segundo simposio de investigación y tecnología de máquinas virtuales Java de USENIX, 2002 (JVM '02)