Si no hay un despacho dinámico (polimorfismo), los "métodos" son solo funciones azucaradas, quizás con un parámetro adicional implícito. En consecuencia, las instancias de clases sin comportamiento polimórfico son esencialmente C struct
s con el propósito de generar código.
Para el despacho dinámico clásico en un sistema de tipo estático, básicamente hay una estrategia predominante: vtables. Cada instancia obtiene un puntero adicional que se refiere a (una representación limitada de) su tipo, lo más importante: vtable: una matriz de punteros de función, uno por método. Como se conoce el conjunto completo de métodos para cada tipo (en la cadena de herencia) en tiempo de compilación, se pueden asignar índices consecutivos (0..N para N métodos) a los métodos e invocarlos buscando el puntero de función en vtable usando este índice (pasando nuevamente la referencia de instancia como parámetro adicional).
Para lenguajes más dinámicos basados en clases, típicamente las propias clases son objetos de primera clase y cada objeto tiene una referencia a su objeto de clase. El objeto de clase, a su vez, posee los métodos de una manera dependiente del lenguaje (en Ruby, los métodos son una parte central del modelo de objetos, en Python son solo objetos de función con pequeños envoltorios a su alrededor). Por lo general, las clases también almacenan referencias a sus superclases y delegan la búsqueda de métodos heredados a esas clases para ayudar a la metaprogramación que agrega y altera los métodos.
Hay muchos otros sistemas que no se basan en clases, pero difieren significativamente, por lo que solo elegiré una alternativa de diseño interesante: cuando puede agregar nuevos (conjuntos de) métodos a todos los tipos a voluntad en cualquier parte del programa ( ej. clases de tipo en Haskell y rasgos en Rust), el conjunto completo de métodos no se conoce durante la compilación. Para resolver esto, uno crea una vtable por rasgo y los pasa cuando se requiere la implementación del rasgo. Es decir, código como este:
void needs_a_trait(SomeTrait &x) { x.method2(1); }
ConcreteType x = ...;
needs_a_trait(x);
se compila a esto:
functionpointer SomeTrait_ConcreteType_vtable[] = { &method1, &method2, ... };
void needs_a_trait(void *x, functionpointer vtable[]) { vtable[1](x, 1); }
ConcreteType x = ...;
needs_a_trait(x, SomeTrait_ConcreteType_vtable);
Esto también significa que la información de vtable no está incrustada en el objeto. Si desea referencias a una "instancia de un rasgo" que se comportará correctamente cuando, por ejemplo, se almacene en estructuras de datos que contienen muchos tipos diferentes, se puede crear un puntero grueso (instance_pointer, trait_vtable)
. Esto es en realidad una generalización de la estrategia anterior.