15.5.2. Pintar funciones virtuales

Para entender exactamente qué está pasando cuando se usan funciones virtuales, es útil ver la actividad que hay detrás del telón. Aquí se muestra el array de punteros A[] in Instrument4.cpp:

Funciones virtuales

Figura 15.1. Funciones virtuales


El array de punteros a Instruments no tiene información específica de tipo; cada uno de ellos apunta a un objeto de tipo Instrument. Wind, Percussion, Stringed, y Brass encajan en esta categoría porque derivan de Instrument (esto hace que tengan la misma interfaz de Instrument, y puedan responder a los mismos mensajes), lo que implica que sus direcciones pueden ser metidas en el array. Sin embargo, el compilador no sabe que sean otra cosa que objetos de tipo Instrument, por lo que normalmente llamará a las versiones de las funciones que estén en la clase base. Pero en este caso, todas las funciones han sido declaradas con la palabra reservada virtual, por lo que ocurre algo diferente. Cada vez que se crea una clase que contiene funciones virtuales, o se deriva de una clase que contiene funciones virtuales, el compilador crea para cada clase una única VTABLE, que se puede ver a la derecha en el diagrama. En ésta tabla se colocan las direcciones de todas las funciones que son declaradas virtuales en la clase o en la clase base. Si no se sobreescribe una función que ha sido declarada como virtual, el compilador usa la dirección de la versión que se encuentra en la clase base (esto se puede ver en la entrada adjusta de la VTABLE de Brass). Además, se coloca el VPTR (descubierto en Sizes.cpp) en la clase. Hay un único VPTR por cada objeto cuando se usa herencia simple como es el caso. El VPTR debe estar inicializado para que apunte a la dirección inicial de la VTABLE apropiada (esto sucede en el constructor que se verá más tarde con mayor detalle).

Una vez que el VPTR ha sido inicializado a la VTABLE apropiada, el objeto "sabe" de que tipo es. Pero este autoconocimiento no tiene valor a menos que sea usado en el momento en que se llama a la función virtual.

Cuando se llama a una función virtual a través de la clase base (la situación que se da cuando el compilador no tiene toda la información necesaria para realizar la ligadura estática), ocurre algo especial. En vez de realizarse la típica llamada a función, que en lenguaje ensamblador es simplemente un CALL a una dirección en concreto, el compilador genera código diferente para ejecutar la llamada a la función. Aquí se muestra a lo que se parece una llamada a adjust() para un objeto Brass, si se hace a través de un puntero a Instrument (una referencia a Instrument produce el mismo efecto):

Tabla de punteros virtuales

Figura 15.2. Tabla de punteros virtuales


El compilador empieza con el puntero a Instrument, que apunta a la dirección inicial del objeto. Todos los objetos Instrument o los objetos derivados de Instrument tienen su VPTR en el mismo lugar (a menudo al principio del objeto), de tal forma que el compilador puede conseguir el VPTR del objeto. El VPTR apunta a la la dirección inicial de VTABLE. Todas las direcciones de funciones de las VTABLE están dispuestas en el mismo orden, a pesar del tipo específico del objeto. play() es el primero, what() es el segundo y adjust() es el tercero. El compilador sabe que a pesar del tipo específico del objeto, la función adjust() se encuentra localizada en VPTR+2. Debido a esto, en vez de decir, "Llama a la función en la dirección absoluta Instrument::adjust() (ligadura estática y acción equivocada), se genera código que dice "Llama a la función que se encuentre en VPTR+2". Como la búsqueda del VPTR y la determinación de la dirección de la función actual ocurre en tiempo de ejecución, se consigue la deseada ligadura dinámica. Se envía un mensaje al objeto, y el objeto se figura que debe hacer con él.