Estaba buscando información sobre la tabla virtual, pero no encuentro nada que sea fácil de entender.
¿Alguien puede darme buenos ejemplos (no de Wiki, por favor) con explicaciones o enlaces?¿Por qué necesitamos mesa virtual?
Respuesta
Sin tablas virtuales no sería posible hacer que el polimorfismo en tiempo de ejecución funcione, ya que todas las referencias a las funciones se vincularían en tiempo de compilación. Un ejemplo sencillo
struct Base {
virtual void f() { }
};
struct Derived : public Base {
virtual void f() { }
};
void callF(Base *o) {
o->f();
}
int main() {
Derived d;
callF(&d);
}
Dentro de la función callF
, es suficiente saber que o
apunta a un objeto Base
. Sin embargo, en tiempo de ejecución, el código debe llamar al Derived::f
(ya que Base::f
es virtual). En tiempo de compilación, el compilador no puede saber qué código va a ejecutar la llamada o->f()
, ya que no sabe a qué apunta o
.
Por lo tanto, necesita algo llamado "tabla virtual" que es básicamente una tabla de indicadores de función. Cada objeto que tiene funciones virtuales tiene un "puntero de tabla v" que apunta a la tabla virtual para objetos de su tipo.
El código en la función callF
anterior solo necesita buscar la entrada para Base::f
en la tabla virtual (que encuentra en función del puntero v-table en el objeto), y luego llama a la función que ingresa a la tabla puntos a. Que podría ser Base::f
, pero también es posible que apunte a algo más - Derived::f
, por ejemplo.
Esto significa que debido a la tabla virtual, puede tener polimorfismo en tiempo de ejecución porque la función real que se llama se determina en tiempo de ejecución buscando un puntero de función en la tabla virtual y luego llamando a la función mediante ese puntero - en lugar de llamar directamente a la función (como es el caso de las funciones no virtuales).
Respuesta corta: llamada de función virtual, basePointer-> f(), significa diferentes cosas dependiendo del historial de basePointer. Si apunta a algo que realmente es una clase derivada, se llamará a una función diferente.
Para esto, el compilador hace un juego simple de indicadores de función. Las direcciones de las funciones que deben invocarse para diferentes tipos se almacenan en la tabla virtual.
La tabla virtual no se usa solo para punteros a funciones. La maquinaria RTTI lo usa para información de tipo de tiempo de ejecución (obteniendo tipos reales de un objeto referenciado por una dirección de uno de los tipos base).
Algunas implementaciones nuevas/borrar almacenarían el tamaño del objeto en la tabla virtual.
La programación COM de Windows usa una tabla virtual para crackearla e insertarla como interfaz.
Para responder a su pregunta principal, no lo hace, y el Estándar C++ no especifica que se le debe proporcionar uno. Lo que sí quiere es poder decir:
struct A {
virtual ~A() {}
virtual void f() {}
};
struct B : public A {
void f() {}
};
A * p = new B;
p->f();
y tener B :: f llamado y no A :: f. Una tabla de funciones virtuales es una forma de implementar esto, pero francamente no es de interés para el programador promedio de C++; solo lo pienso al responder preguntas como esta.
Para un ejemplo de alternativa, python almacena los métodos junto con los atributos directamente en el objeto. De este modo, logra este comportamiento sin usar una tabla 'virtual', aunque es muy similar. –
@Matthieu M, python hace exactamente lo mismo que C++ - almacena alguna referencia a un objeto de método (en c-python implementado con punteros), mientras que C++ almacena la dirección del método. La diferencia radica principalmente en que las tablas python se almacenan por objeto y no por clase, ya que Python permite agregar atributos y métodos en el tiempo de ejecución. – gnud
En realidad estoy en desacuerdo: en C++ la tabla virtual no tiene información real sobre los punteros para la función almacenada, el compilador simplemente sabe que el método requerido está en un índice dado.Por otro lado en python, los atributos y métodos se almacenan (generalmente) en un dictionario y usted hace su búsqueda por nombre. Es una gran diferencia en la implementación, que le da a python más flexibilidad a costa del rendimiento. –
Supongamos Player
y Monster
heredan de una clase base abstracta Actor
que define una operación virtual name()
. Además suponga que tiene una función que pide a un actor por su nombre:
void print_information(const Actor& actor)
{
std::cout << "the actor is called " << actor.name() << std::endl;
}
Es imposible deducir en tiempo de compilación si el actor será en realidad un jugador o un monstruo. Dado que tienen diferentes métodos name()
, la decisión sobre qué método llamar debe diferirse hasta el tiempo de ejecución. El compilador agrega información adicional a cada objeto actor que permite que esta decisión se tome en tiempo de ejecución.
En cada compilador que sé, esta información adicional es un puntero (a menudo llamado VPTR) a una tabla de punteros de función (a menudo llamadas VTBL) que son específicos de la clase concreta. Es decir, todos los objetos del jugador comparten la misma tabla virtual que contiene punteros a todos los métodos del jugador (lo mismo vale para los monstruos). En tiempo de ejecución, el método correcto se encuentra eligiendo el método del vtbl apuntado por el vptr del objeto sobre el que se debe invocar el método.
La tabla de funciones virtuales es un detalle de implementación, es la forma en que el compilador implementa los métodos polimórficos en las clases.
Considere
class Animal
{
virtual void talk()=0;
}
class Dog : Animal
{
virtual void talk() {
cout << "Woof!";
}
}
class Cat : Animal
{
virtual void talk() {
cout << "Meow!";
}
}
Y ahora tenemos
A* animal = loadFromFile("somefile.txt"); // from somewhere
animal->talk();
¿Cómo saber qué versión de talk()
se llama? El objeto animal tiene una tabla que apunta a las funciones virtuales que se usan con ese animal. Por ejemplo, puede haber talk
en la tercera offset, si hay otros dos métodos virtuales:
dog
[function ptr for some method 1]
[function ptr for some method 2]
[function ptr for talk -> Dog::Talk]
cat
[function ptr for some method 1]
[function ptr for some method 2]
[function ptr for talk -> Cat::Talk]
Cuando tenemos una instancia de Animnal
, no sabemos lo que talk()
método para llamar. Lo encontramos buscando en la tabla virtual y buscando la tercera entrada, ya que el compilador sabe que corresponde al puntero talk
(el compilador conoce los métodos virtuales en Animal, y así conoce el orden de los punteros en el vtable).
Dado un Animal, para llamar al método de talk() correcto, el compilador agrega código para buscar el puntero de la tercera función y usar eso. Esto luego dirige a la implementación apropiada.
Con métodos no virtuales, esto no es necesario ya que la función real que se llama se puede determinar en tiempo de compilación: solo hay una función posible que se puede llamar para una llamada no virtual.
A * animal = ....; // desde algún lugar animal-> talk(); en esta línea puede Usted ser más específico, gracias – lego69
Claro - imagine que el animal está cargado desde el archivo, o se crea un animal diferente dependiendo de la entrada del usuario. La intención es que el compilador no tenga forma de saber qué tipo de animal tenemos. – mdma
- 1. mesa virtual/mesa de despacho
- 2. ¿Por qué necesitamos fibras
- 3. ¿Por qué necesitamos Thread.MemoryBarrier()?
- 4. ¿Por qué necesitamos C Unions?
- 5. ¿Por qué necesitamos ng-click?
- 6. ¿Por qué necesitamos usar Radix?
- 7. ¿Por qué necesitamos marcos burlones?
- 8. ¿Por qué necesitamos web-sockets?
- 9. ¿Por qué necesitamos struct? (C#)
- 10. ¿Por qué necesitamos el método ContinueWith?
- 11. ¿Por qué necesitamos una etiqueta fieldset?
- 12. ¿Por qué necesitamos este operador especial ===?
- 13. ¿Por qué necesitamos Propiedades en C#
- 14. ¿Por qué necesitamos Application Server en Java?
- 15. ¿Por qué todavía necesitamos código generado?
- 16. ¿Por qué necesitamos un constructor privado?
- 17. ¿por qué necesitamos ClassMethods e InstanceMethods?
- 18. ¿Por qué necesitamos parámetros de "salida"?
- 19. ¿Por qué incluso necesitamos el operador "delete []"?
- 20. ¿Por qué necesitamos servicios web RESTful?
- 21. ¿Por qué necesitamos delegados de C#
- 22. ¿Por qué necesitamos patrones de diseño?
- 23. ¿Por qué necesitamos interfaces en Java?
- 24. ¿Por qué necesitamos funcall en Lisp?
- 25. ¿por qué necesitamos zone_highmem en x86?
- 26. Patrón de diseño del generador: ¿por qué necesitamos un Director?
- 27. ¿Qué es __i686.get_pc_thunk.bx? ¿Por qué necesitamos esta llamada?
- 28. PHP: ¿Qué son construcciones idiomáticas y por qué las necesitamos?
- 29. Marshalling: ¿qué es y por qué lo necesitamos?
- 30. ¿Por qué usamos Virtual y Override?
* Usted * no lo necesita, pero el compilador sí. – EJP