15.5. Cómo implementa C++ la ligadura dinámica

¿Cómo funciona la ligadura dinámica? Todo el trabajo se realiza detrás del telón gracias al compilador, que instala los mecanismos necesarios de la ligadura dinámica cuando se crean funciones virtuales. Debido a que los programadores se suelen beneficiar de la comprensión del mecanismo de las funciones virtuales en C++, esta sección mostrará la forma en que el compilador implementa este mecanismo.

La palabra reservada virtual le dice al compilador que no debe realizar ligadura estática. Al contrario, debe instalar automáticamente todos los mecanismos necesarios para realizar la ligadura dinámica. Esto significa que si se llama a play() para un objeto Brass a través una dirección a la clase base Instrument, se usará la función apropiada.

Para que funcione, el compilador típico [75] crea una única tabla (llamada VTABLE) por cada clase que contenga funciones virtuales. El compilador coloca las direcciones de las funciones virtuales de esa clase en concreto en la VTABLE. En cada clase con funciones virtuales el compilador coloca de forma secreta un puntero llamado vpointer (de forma abreviada VPTR), que apunta a la VTABLE de ese objeto. Cuando se hace una llamada a una función virtual a través de un puntero a la clase base (es decir, cuando se hace una llamada usando el polimorfismo), el compilador silenciosamente añade código para buscar el VPTR y así conseguir la dirección de la función en la VTABLE, con lo que se llama a la función correcta y tiene lugar la ligadura dinámica.

Todo esto - establecer la VTABLE para cada clase, inicializar el VPTR, insertar código para la llamada a la función virtual - sucede automáticamente sin que haya que preocuparse por ello. Con las funciones virtuales, se llama a la función apropiada de un objeto, incluso aunque el compilador no sepa el tipo exacto del objeto.

15.5.1. Almacenando información de tipo

Se puede ver que no hay almacenada información de tipo de forma explícita en ninguna de las clases. Pero los ejemplos anteriores, y la simple lógica, dicen que debe existir algún tipo de información almacenada en los objetos; de otra forma el tipo no podría ser establecido en tiempo de ejecución. Es verdad, pero la información de tipo está oculta. Para verlo, aquí está un ejemplo que muestra el tamaño de las clases que usan funciones virtuales comparadas con aquellas que no las usan:

//: C15:Sizes.cpp
// Object sizes with/without virtual functions
#include <iostream>
using namespace std;

class NoVirtual {
  int a;
public:
  void x() const {}
  int i() const { return 1; }
};

class OneVirtual {
  int a;
public:
  virtual void x() const {}
  int i() const { return 1; }
};

class TwoVirtuals {
  int a;
public:
  virtual void x() const {}
  virtual int i() const { return 1; }
};

int main() {
  cout << "int: " << sizeof(int) << endl;
  cout << "NoVirtual: "
       << sizeof(NoVirtual) << endl;
  cout << "void* : " << sizeof(void*) << endl;
  cout << "OneVirtual: "
       << sizeof(OneVirtual) << endl;
  cout << "TwoVirtuals: "
       << sizeof(TwoVirtuals) << endl;
} ///:~

Listado 15.4. C15/Sizes.cpp


Sin funciones virtuales el tamaño del objeto es exactamente el que se espera: el tamaño de un único [76] int. Con una única función virtual en OneVirtual, el tamaño del objeto es el tamaño de NoVirtual más el tamaño de un puntero a void. Lo que implica que el compilador añade un único puntero (el VPTR) en la estructura si se tienen una o más funciones virtuales. No hay diferencia de tamaño entre OneVirtual y TwoVirtuals. Esto es porque el VPTR apunta a una tabla con direcciones de funciones. Se necesita sólo una tabla porque todas las direcciones de las funciones virtuales están contenidas en esta tabla.

Este ejemplo requiere como mínimo un miembro de datos. Si no hubiera miembros de datos, el compilador de C++ hubiera forzado a los objetos a ser de tamaño no nulo porque cada objeto debe tener direcciones distintas (¿se imagina cómo indexar un array de objetos de tamaño nulo?). Por esto se inserta en el objeto un miembro "falso" ya que de otra forma tendríá un tamaño nulo. Cuando se inserta la información de tipo gracias a la palabra reservada virtual, ésta ocupa el lugar del miembro "falso". Intente comentar el int a en todas las clases del ejemplo anterior para comprobarlo.



[75] Los compiladores pueden implementar el comportamiento virtual como quieran, pero el modo aquí descrito es una aproximación casi universal.

[76] Algunos compiladores pueden aumentar el tamaño pero sería raro.