¿Cómo se implementan las funciones virtuales y vtable?


109

Todos sabemos qué funciones virtuales hay en C ++, pero ¿cómo se implementan a un nivel profundo?

¿Se puede modificar vtable o incluso acceder directamente en tiempo de ejecución?

¿Existe vtable para todas las clases, o solo para aquellas que tienen al menos una función virtual?

¿Las clases abstractas simplemente tienen un NULL para el puntero de función de al menos una entrada?

¿Tener una sola función virtual ralentiza a toda la clase? ¿O solo la llamada a la función que es virtual? ¿Y la velocidad se ve afectada si la función virtual se sobrescribe o no, o esto no tiene ningún efecto mientras sea virtual?


2
Sugiera leer la obra maestra Inside the C++ Object Modelpor Stanley B. Lippman. (Sección 4.2, página 124-131)
smwikipedia

Respuestas:


123

¿Cómo se implementan las funciones virtuales a un nivel profundo?

De "Funciones virtuales en C ++" :

Siempre que un programa tiene una función virtual declarada, se construye av - table para la clase. La tabla v consta de direcciones a las funciones virtuales para clases que contienen una o más funciones virtuales. El objeto de la clase que contiene la función virtual contiene un puntero virtual que apunta a la dirección base de la tabla virtual en la memoria. Siempre que hay una llamada de función virtual, la tabla v se usa para resolver la dirección de la función. Un objeto de la clase que contiene una o más funciones virtuales contiene un puntero virtual llamado vptr al comienzo del objeto en la memoria. Por tanto, el tamaño del objeto en este caso aumenta con el tamaño del puntero. Este vptr contiene la dirección base de la tabla virtual en la memoria. Tenga en cuenta que las tablas virtuales son específicas de la clase, es decir, solo hay una tabla virtual para una clase, independientemente del número de funciones virtuales que contiene. Esta tabla virtual, a su vez, contiene las direcciones base de una o más funciones virtuales de la clase. En el momento en que se llama a una función virtual en un objeto, el vptr de ese objeto proporciona la dirección base de la tabla virtual para esa clase en la memoria. Esta tabla se utiliza para resolver la llamada de función ya que contiene las direcciones de todas las funciones virtuales de esa clase. Así es como se resuelve el enlace dinámico durante una llamada de función virtual. el vptr de ese objeto proporciona la dirección base de la tabla virtual para esa clase en la memoria. Esta tabla se utiliza para resolver la llamada de función ya que contiene las direcciones de todas las funciones virtuales de esa clase. Así es como se resuelve el enlace dinámico durante una llamada de función virtual. el vptr de ese objeto proporciona la dirección base de la tabla virtual para esa clase en la memoria. Esta tabla se utiliza para resolver la llamada de función ya que contiene las direcciones de todas las funciones virtuales de esa clase. Así es como se resuelve el enlace dinámico durante una llamada de función virtual.

¿Se puede modificar vtable o incluso acceder directamente en tiempo de ejecución?

Universalmente, creo que la respuesta es "no". Podría modificar la memoria para encontrar la tabla vtable, pero aún así no sabría cómo se ve la firma de la función para llamarla. Todo lo que desee lograr con esta capacidad (que admite el lenguaje) debería ser posible sin acceso a la tabla vtable directamente o sin modificarla en tiempo de ejecución. También tenga en cuenta que la especificación del lenguaje C ++ no especifica que se requieran vtables; sin embargo, así es como la mayoría de los compiladores implementan funciones virtuales.

¿Existe vtable para todos los objetos, o solo para aquellos que tienen al menos una función virtual?

Yo creo la respuesta aquí es "depende de la implementación" ya que la especificación no requiere vtables en primer lugar. Sin embargo, en la práctica, creo que todos los compiladores modernos solo crean una vtable si una clase tiene al menos 1 función virtual. Hay una sobrecarga de espacio asociada con la vtable y una sobrecarga de tiempo asociada con llamar a una función virtual frente a una función no virtual.

¿Las clases abstractas simplemente tienen un NULL para el puntero de función de al menos una entrada?

La respuesta es que no está especificado por la especificación del idioma, por lo que depende de la implementación. Llamar a la función virtual pura da como resultado un comportamiento indefinido si no está definido (que normalmente no lo está) (ISO / IEC 14882: 2003 10.4-2). En la práctica, asigna un espacio en la vtable para la función pero no le asigna una dirección. Esto deja la vtable incompleta, lo que requiere que las clases derivadas implementen la función y completen la vtable. Algunas implementaciones simplemente colocan un puntero NULL en la entrada vtable; otras implementaciones colocan un puntero a un método ficticio que hace algo similar a una aserción.

Tenga en cuenta que una clase abstracta puede definir una implementación para una función virtual pura, pero esa función solo se puede llamar con una sintaxis de id calificado (es decir, especificando completamente la clase en el nombre del método, similar a llamar a un método de clase base desde un clase derivada). Esto se hace para proporcionar una implementación predeterminada fácil de usar, sin dejar de requerir que una clase derivada proporcione una anulación.

¿Tener una sola función virtual ralentiza toda la clase o solo la llamada a la función que es virtual?

Esto está llegando al límite de mi conocimiento, ¡así que alguien me ayude si me equivoco!

Yo creo que sólo las funciones que son virtual en la experiencia de clase del rendimiento en tiempo golpeó relacionada con llamar a una función virtual frente a una función no virtual. El espacio para la clase está ahí de cualquier manera. Tenga en cuenta que si hay una vtable, solo hay 1 por clase , no uno por objeto .

¿Se ve afectada la velocidad si la función virtual se anula o no, o esto no tiene ningún efecto mientras sea virtual?

No creo que el tiempo de ejecución de una función virtual que se anula disminuya en comparación con la llamada a la función virtual base. Sin embargo, hay una sobrecarga de espacio adicional para la clase asociada con la definición de otra vtable para la clase derivada frente a la clase base.

Recursos adicionales:

http://www.codersource.net/published/view/325/virtual_functions_in.aspx (a través de la máquina de retorno)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/ cxx-abi / abi.html # vtable


2
No estaría en línea con la filosofía de C ++ de Stroustrup que un compilador colocara un puntero vtable innecesario en un objeto que no lo necesita. La regla es que no se obtiene una sobrecarga que no esté en C a menos que la solicite, y es de mala educación que los compiladores lo rompan.
Steve Jessop

3
Estoy de acuerdo en que sería una tontería para cualquier compilador que se toma en serio usar una vtable cuando no existen funciones virtuales. Sin embargo, me pareció importante señalar que, que yo sepa, el estándar C ++ no lo requiere, así que tenga cuidado antes de depender de él.
Zach Burlingame

8
Incluso las funciones virtuales se pueden llamar de forma no virtual. De hecho, esto es bastante común: si el objeto está en la pila, dentro del alcance, el compilador sabrá el tipo exacto y optimizará la búsqueda de vtable. Esto es especialmente cierto para el dtor, que debe llamarse en el mismo ámbito de pila.
MSalters

1
Creo que cuando una clase tiene al menos una función virtual, cada objeto tiene una vtable, y no una para toda la clase.
Asaf R

3
Implementación común: cada objeto tiene un puntero a una vtable; la clase es dueña de la mesa. La magia de la construcción consiste simplemente en actualizar el puntero vtable en el ctor derivado, después de que el ctor base haya terminado.
MSalters

31
  • ¿Se puede modificar vtable o incluso acceder directamente en tiempo de ejecución?

No portátil, pero si no te importan los trucos sucios, ¡seguro!

ADVERTENCIA : Esta técnica no se recomienda para niños, adultos menores de 969 años o pequeñas criaturas peludas de Alpha Centauri. Los efectos secundarios pueden incluir demonios que salen volando de su nariz , la aparición abrupta de Yog-Sothoth como un aprobador requerido en todas las revisiones de código posteriores, o la adición retroactiva de IHuman::PlayPiano()a todas las instancias existentes]

En la mayoría de los compiladores que he visto, vtbl * son los primeros 4 bytes del objeto, y el contenido de vtbl es simplemente una matriz de punteros de miembros (generalmente en el orden en que fueron declarados, con el primero de la clase base). Por supuesto, hay otros diseños posibles, pero eso es lo que he observado generalmente.

class A {
  public:
  virtual int f1() = 0;
};
class B : public A {
  public:
  virtual int f1() { return 1; }
  virtual int f2() { return 2; }
};
class C : public A {
  public:
  virtual int f1() { return -1; }
  virtual int f2() { return -2; }
};

A *x = new B;
A *y = new C;
A *z = new C;

Ahora para hacer algunas travesuras ...

Cambio de clase en tiempo de ejecución:

std::swap(*(void **)x, *(void **)y);
// Now x is a C, and y is a B! Hope they used the same layout of members!

Reemplazar un método para todas las instancias (parchear una clase)

Este es un poco más complicado, ya que el propio vtbl probablemente esté en la memoria de solo lectura.

int f3(A*) { return 0; }

mprotect(*(void **)x,8,PROT_READ|PROT_WRITE|PROT_EXEC);
// Or VirtualProtect on win32; this part's very OS-specific
(*(int (***)(A *)x)[0] = f3;
// Now C::f1() returns 0 (remember we made x into a C above)
// so x->f1() and z->f1() both return 0

Es muy probable que esto último haga que los verificadores de virus y el enlace se activen y se den cuenta, debido a las manipulaciones de mprotect. En un proceso que utiliza el bit NX, puede fallar.


6
Hmm. Se siente siniestro que esto haya recibido una recompensa. Espero que eso no signifique que @Mobilewits piense que tales travesuras son en realidad una buena idea ...
puetzk

1
Considere desalentar el uso de esta técnica, de manera clara y contundente, en lugar de "guiñar un ojo".
einpoklum

" Los contenidos de vtbl son simplemente una matriz de punteros de miembros " en realidad es un registro (una estructura) con diferentes entradas, que resultan estar espaciadas de manera uniforme
curioso

1
Puedes mirarlo de cualquier manera; los punteros de función tienen diferentes firmas y, por lo tanto, diferentes tipos de punteros; en ese sentido, es de hecho similar a una estructura. Pero en otros contextos, pero la idea de índice vtbl es útil (por ejemplo, ActiveX lo usa en la forma en que describe interfaces duales en typelibs), que es una vista más parecida a una matriz.
puetzk

17

¿Tener una sola función virtual ralentiza a toda la clase?

¿O solo la llamada a la función que es virtual? ¿Y la velocidad se ve afectada si la función virtual se sobrescribe o no, o esto no tiene ningún efecto mientras sea virtual?

Tener funciones virtuales ralentiza toda la clase en la medida en que un dato más tiene que ser inicializado, copiado,… cuando se trata de un objeto de tal clase. Para una clase con media docena de miembros aproximadamente, la diferencia debería ser insignificante. Para una clase que solo contiene un charmiembro, o ningún miembro, la diferencia puede ser notable.

Aparte de eso, es importante tener en cuenta que no todas las llamadas a una función virtual son llamadas a funciones virtuales. Si tiene un objeto de un tipo conocido, el compilador puede emitir código para una invocación de función normal, e incluso puede insertar dicha función en línea si lo desea. Solo cuando realiza llamadas polimórficas, a través de un puntero o referencia que podría apuntar a un objeto de la clase base o un objeto de alguna clase derivada, necesita la indirección vtable y paga por ella en términos de rendimiento.

struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
  Foo x; x.a(); // non-virtual: always calls Foo::a()
  Bar y; y.a(); // non-virtual: always calls Bar::a()
  arg.a();      // virtual: must dispatch via vtable
  Foo z = arg;  // copy constructor Foo::Foo(const Foo&) will convert to Foo
  z.a();        // non-virtual Foo::a, since z is a Foo, even if arg was not
}

Los pasos que debe seguir el hardware son esencialmente los mismos, sin importar si la función se sobrescribe o no. La dirección de la vtable se lee desde el objeto, el puntero de función se recupera de la ranura correspondiente y la función se llama mediante puntero. En términos de rendimiento real, las predicciones de rama pueden tener algún impacto. Entonces, por ejemplo, si la mayoría de sus objetos se refieren a la misma implementación de una función virtual dada, entonces existe la posibilidad de que el predictor de rama prediga correctamente a qué función llamar incluso antes de que se haya recuperado el puntero. Pero no importa qué función sea la común: podría ser la mayoría de los objetos delegando al caso base no sobrescrito, o la mayoría de los objetos que pertenecen a la misma subclase y, por lo tanto, delegando al mismo caso sobrescrito.

¿Cómo se implementan a un nivel profundo?

Me gusta la idea de jheriko para demostrar esto usando una implementación simulada. Pero usaría C para implementar algo similar al código anterior, de modo que el nivel bajo se vea más fácilmente.

clase padre Foo

typedef struct Foo_t Foo;   // forward declaration
struct slotsFoo {           // list all virtual functions of Foo
  const void *parentVtable; // (single) inheritance
  void (*destructor)(Foo*); // virtual destructor Foo::~Foo
  int (*a)(Foo*);           // virtual function Foo::a
};
struct Foo_t {                      // class Foo
  const struct slotsFoo* vtable;    // each instance points to vtable
};
void destructFoo(Foo* self) { }     // Foo::~Foo
int aFoo(Foo* self) { return 1; }   // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
  0,                                // no parent class
  destructFoo,
  aFoo
};
void constructFoo(Foo* self) {      // Foo::Foo()
  self->vtable = &vtableFoo;        // object points to class vtable
}
void copyConstructFoo(Foo* self,
                      Foo* other) { // Foo::Foo(const Foo&)
  self->vtable = &vtableFoo;        // don't copy from other!
}

barra de clase derivada

typedef struct Bar_t {              // class Bar
  Foo base;                         // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { }     // Bar::~Bar
int aBar(Bar* self) { return 2; }   // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
  &vtableFoo,                       // can dynamic_cast to Foo
  (void(*)(Foo*)) destructBar,      // must cast type to avoid errors
  (int(*)(Foo*)) aBar
};
void constructBar(Bar* self) {      // Bar::Bar()
  self->base.vtable = &vtableBar;   // point to Bar vtable
}

función f realizando una llamada de función virtual

void f(Foo* arg) {                  // same functionality as above
  Foo x; constructFoo(&x); aFoo(&x);
  Bar y; constructBar(&y); aBar(&y);
  arg->vtable->a(arg);              // virtual function call
  Foo z; copyConstructFoo(&z, arg);
  aFoo(&z);
  destructFoo(&z);
  destructBar(&y);
  destructFoo(&x);
}

Como puede ver, una vtable es solo un bloque estático en la memoria, que en su mayoría contiene punteros de función. Cada objeto de una clase polimórfica apuntará a la vtable correspondiente a su tipo dinámico. Esto también hace que la conexión entre RTTI y las funciones virtuales sea más clara: puede verificar qué tipo es una clase simplemente mirando a qué vtable apunta. Lo anterior se simplifica de muchas maneras, como por ejemplo, herencia múltiple, pero el concepto general es sólido.

Si arges de tipo Foo*y lo tomas arg->vtable, pero en realidad es un objeto de tipo Bar, obtendrás la dirección correcta del vtable. Eso es porque vtablesiempre es el primer elemento en la dirección del objeto, sin importar si se llama vtableo base.vtableen una expresión escrita correctamente.


"Cada objeto de una clase polimórfica apuntará a su propia tabla v". ¿Estás diciendo que cada objeto tiene su propia vtable? AFAIK vtable se comparte entre todos los objetos de la misma clase. Avísame si me equivoco.
Bhuwan

1
@Bhuwan: No, tienes razón: solo hay una vtable por tipo (que podría ser por instanciación de plantilla en el caso de las plantillas). Quería decir que cada objeto de una clase polimórfica apunta a la vtable que se le aplica, por lo que cada objeto tiene ese puntero, pero para objetos del mismo tipo apuntará a la misma tabla. Probablemente debería reformular esto.
MvG

1
@MvG "los objetos del mismo tipo apuntarán a la misma tabla " ¡no durante la construcción de clases base con clases base virtuales! (un caso muy especial)
curioso

1
@curiousguy: Archivaría eso bajo “lo anterior se simplifica de muchas maneras”, particularmente porque la aplicación principal de las bases virtuales es la herencia múltiple, que tampoco modelé. Pero gracias por el comentario, es útil tener esto aquí para las personas que puedan necesitar más profundidad.
MvG


2

Esta respuesta se ha incorporado al respuesta de Community Wiki

  • ¿Las clases abstractas simplemente tienen un NULL para el puntero de función de al menos una entrada?

La respuesta para eso es que no está especificado: llamar a la función virtual pura da como resultado un comportamiento indefinido si no está definido (que generalmente no lo está) (ISO / IEC 14882: 2003 10.4-2). Algunas implementaciones simplemente colocan un puntero NULL en la entrada vtable; otras implementaciones colocan un puntero a un método ficticio que hace algo similar a una aserción.

Tenga en cuenta que una clase abstracta puede definir una implementación para una función virtual pura, pero esa función solo se puede llamar con una sintaxis de id calificado (es decir, especificando completamente la clase en el nombre del método, similar a llamar a un método de clase base desde un clase derivada). Esto se hace para proporcionar una implementación predeterminada fácil de usar, sin dejar de requerir que una clase derivada proporcione una anulación.


Además, no creo que una clase abstracta pueda definir una implementación para una función virtual pura. Por definición, una función virtual pura no tiene cuerpo (por ejemplo, bool my_func () = 0;). Sin embargo, puede proporcionar implementaciones para funciones virtuales regulares.
Zach Burlingame

Una función virtual pura puede tener una definición. Consulte el artículo n. ° 34 de "C ++ efectivo, tercera edición" de Scott Meyers, ISO 14882-2003 10.4-2, o bytes.com/forum/thread572745.html
Michael Burr

2

Puede recrear la funcionalidad de funciones virtuales en C ++ utilizando punteros de función como miembros de una clase y funciones estáticas como implementaciones, o utilizando puntero a funciones miembro y funciones miembro para las implementaciones. Solo hay ventajas de notación entre los dos métodos ... de hecho, las llamadas a funciones virtuales son solo una conveniencia de notación en sí mismas. De hecho, la herencia es solo una conveniencia de notación ... todo se puede implementar sin usar las características del lenguaje para la herencia. :)

Lo siguiente es una mierda sin probar, probablemente un código con errores, pero con suerte demuestra la idea.

p.ej

class Foo
{
protected:
 void(*)(Foo*) MyFunc;
public:
 Foo() { MyFunc = 0; }
 void ReplciatedVirtualFunctionCall()
 {
  MyFunc(*this);
 }
...
};

class Bar : public Foo
{
private:
 static void impl1(Foo* f)
 {
  ...
 }
public:
 Bar() { MyFunc = impl1; }
...
};

class Baz : public Foo
{
private:
 static void impl2(Foo* f)
 {
  ...
 }
public:
 Baz() { MyFunc = impl2; }
...
};

void(*)(Foo*) MyFunc;¿Es esto alguna sintaxis de Java?
curioso

no, su sintaxis C / C ++ para punteros de función. Para citarme, "Puede recrear la funcionalidad de funciones virtuales en C ++ usando punteros de función". es una sintaxis desagradable, pero algo con lo que debe estar familiarizado si se considera un programador en C.
jheriko

El puntero de función ac se parecería más a: int ( PROC) (); y un puntero a una función miembro de clase se vería así: int (ClassName :: MPROC) ();
Amenaza

1
@menace, olvidaste algo de sintaxis ahí ... ¿estás pensando en typedef, tal vez? typedef int (* PROC) (); para que pueda hacer PROC foo más tarde en lugar de int (* foo) ()?
jheriko

2

Intentaré hacerlo simple :)

Todos sabemos qué funciones virtuales hay en C ++, pero ¿cómo se implementan a un nivel profundo?

Esta es una matriz con punteros a funciones, que son implementaciones de una función virtual en particular. Un índice en esta matriz representa un índice particular de una función virtual definida para una clase. Esto incluye funciones virtuales puras.

Cuando una clase polimórfica deriva de otra clase polimórfica, podemos tener las siguientes situaciones:

  • La clase derivada no agrega nuevas funciones virtuales ni anula ninguna. En este caso, esta clase comparte la vtable con la clase base.
  • La clase derivada agrega y reemplaza métodos virtuales. En este caso, obtiene su propia vtable, donde las funciones virtuales agregadas tienen un índice que comienza después del último derivado.
  • Varias clases polimórficas en la herencia. En este caso tenemos un cambio de índice entre la segunda y la siguiente base y el índice de la misma en la clase derivada

¿Se puede modificar vtable o incluso acceder directamente en tiempo de ejecución?

No de forma estándar, no hay API para acceder a ellos. Los compiladores pueden tener algunas extensiones o API privadas para acceder a ellos, pero puede que sea solo una extensión.

¿Existe vtable para todas las clases, o solo para aquellas que tienen al menos una función virtual?

Solo aquellos que tienen al menos una función virtual (ya sea destructor) o derivan al menos una clase que tiene su vtable ("es polimórfica").

¿Las clases abstractas simplemente tienen un NULL para el puntero de función de al menos una entrada?

Esa es una implementación posible, pero no se practica. En su lugar, suele haber una función que imprime algo como "función virtual pura llamada" y lo hace abort(). La llamada a eso puede ocurrir si intenta llamar al método abstracto en el constructor o destructor.

¿Tener una sola función virtual ralentiza a toda la clase? ¿O solo la llamada a la función que es virtual? ¿Y la velocidad se ve afectada si la función virtual se sobrescribe o no, o esto no tiene ningún efecto mientras sea virtual?

La ralentización solo depende de si la llamada se resuelve como llamada directa o virtual. Y nada más importa. :)

Si llama a una función virtual a través de un puntero o una referencia a un objeto, entonces siempre se implementará como llamada virtual, porque el compilador nunca puede saber qué tipo de objeto se asignará a este puntero en tiempo de ejecución y si es de un clase en la que este método se anula o no. Solo en dos casos el compilador puede resolver la llamada a una función virtual como una llamada directa:

  • Si llama al método a través de un valor (una variable o resultado de una función que devuelve un valor), en este caso, el compilador no tiene dudas de cuál es la clase real del objeto y puede "resolverlo" en tiempo de compilación. .
  • Si el método virtual está declarado finalen la clase a la que tiene un puntero o referencia a través de la cual lo llama ( solo en C ++ 11 ). En este caso, el compilador sabe que este método no puede sufrir más modificaciones y solo puede ser el método de esta clase.

Sin embargo, tenga en cuenta que las llamadas virtuales solo tienen una sobrecarga de desreferenciar dos punteros. Usar RTTI (aunque solo está disponible para clases polimórficas) es más lento que llamar a métodos virtuales, si encuentra un caso para implementar lo mismo de dos maneras. Por ejemplo, definir virtual bool HasHoof() { return false; }y luego anular solo como bool Horse::HasHoof() { return true; }le proporcionaría la capacidad de llamar if (anim->HasHoof())que será más rápido que intentarlo if(dynamic_cast<Horse*>(anim)). Esto se debe a que dynamic_casttiene que recorrer la jerarquía de clases en algunos casos incluso de forma recursiva para ver si se puede construir la ruta a partir del tipo de puntero real y el tipo de clase deseado. Si bien la llamada virtual es siempre la misma: desreferenciar dos punteros.


2

Aquí hay una implementación manual ejecutable de una tabla virtual en C ++ moderno. Tiene una semántica bien definida, sin hacks y no void*.

Nota: .*y ->*son operadores diferentes a *y ->. Los punteros de función de miembro funcionan de manera diferente.

#include <iostream>
#include <vector>
#include <memory>

struct vtable; // forward declare, we need just name

class animal
{
public:
    const std::string& get_name() const { return name; }

    // these will be abstract
    bool has_tail() const;
    bool has_wings() const;
    void sound() const;

protected: // we do not want animals to be created directly
    animal(const vtable* vtable_ptr, std::string name)
    : vtable_ptr(vtable_ptr), name(std::move(name)) { }

private:
    friend vtable; // just in case for non-public methods

    const vtable* const vtable_ptr;
    std::string name;
};

class cat : public animal
{
public:
    cat(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does meow\n"; 
    }
};

class dog : public animal
{
public:
    dog(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does whoof\n"; 
    }
};

class parrot : public animal
{
public:
    parrot(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return false; }
    bool has_wings() const { return true; }
    void sound() const
    {
        std::cout << get_name() << " does crrra\n"; 
    }
};

// now the magic - pointers to member functions!
struct vtable
{
    bool (animal::* const has_tail)() const;
    bool (animal::* const has_wings)() const;
    void (animal::* const sound)() const;

    // constructor
    vtable (
        bool (animal::* const has_tail)() const,
        bool (animal::* const has_wings)() const,
        void (animal::* const sound)() const
    ) : has_tail(has_tail), has_wings(has_wings), sound(sound) { }
};

// global vtable objects
const vtable vtable_cat(
    static_cast<bool (animal::*)() const>(&cat::has_tail),
    static_cast<bool (animal::*)() const>(&cat::has_wings),
    static_cast<void (animal::*)() const>(&cat::sound));
const vtable vtable_dog(
    static_cast<bool (animal::*)() const>(&dog::has_tail),
    static_cast<bool (animal::*)() const>(&dog::has_wings),
    static_cast<void (animal::*)() const>(&dog::sound));
const vtable vtable_parrot(
    static_cast<bool (animal::*)() const>(&parrot::has_tail),
    static_cast<bool (animal::*)() const>(&parrot::has_wings),
    static_cast<void (animal::*)() const>(&parrot::sound));

// set vtable pointers in constructors
cat::cat(std::string name) : animal(&vtable_cat, std::move(name)) { }
dog::dog(std::string name) : animal(&vtable_dog, std::move(name)) { }
parrot::parrot(std::string name) : animal(&vtable_parrot, std::move(name)) { }

// implement dynamic dispatch
bool animal::has_tail() const
{
    return (this->*(vtable_ptr->has_tail))();
}

bool animal::has_wings() const
{
    return (this->*(vtable_ptr->has_wings))();
}

void animal::sound() const
{
    (this->*(vtable_ptr->sound))();
}

int main()
{
    std::vector<std::unique_ptr<animal>> animals;
    animals.push_back(std::make_unique<cat>("grumpy"));
    animals.push_back(std::make_unique<cat>("nyan"));
    animals.push_back(std::make_unique<dog>("doge"));
    animals.push_back(std::make_unique<parrot>("party"));

    for (const auto& a : animals)
        a->sound();

    // note: destructors are not dispatched virtually
}

1

Cada objeto tiene un puntero vtable que apunta a una matriz de funciones miembro.


1

Algo que no se menciona aquí en todas estas respuestas es que, en caso de herencia múltiple, todas las clases base tienen métodos virtuales. La clase heredada tiene varios punteros a un vmt. El resultado es que el tamaño de cada instancia de tal objeto es mayor. Todo el mundo sabe que una clase con métodos virtuales tiene 4 bytes extra para el vmt, pero en caso de herencia múltiple es para cada clase base que tiene métodos virtuales multiplicado por 4, siendo el tamaño del puntero.


0

Las respuestas de Burly son correctas aquí excepto por la pregunta:

¿Las clases abstractas simplemente tienen un NULL para el puntero de función de al menos una entrada?

La respuesta es que no se crea ninguna tabla virtual para las clases abstractas. ¡No es necesario ya que no se pueden crear objetos de estas clases!

En otras palabras, si tenemos:

class B { ~B() = 0; }; // Abstract Base class
class D : public B { ~D() {} }; // Concrete Derived class

D* pD = new D();
B* pB = pD;

El puntero vtbl al que se accede a través de pB será el vtbl de la clase D. Así es exactamente como se implementa el polimorfismo. Es decir, cómo se accede a los métodos D a través de pB. No es necesario un vtbl para la clase B.

En respuesta al comentario de Mike a continuación ...

Si la clase B en mi descripción tiene un método virtual foo () que no está anulado por D y una barra de método virtual () que está anulada, entonces el vtbl de D tendrá un puntero al foo () de B y a su propia barra () . Todavía no se ha creado ningún vtbl para B.


Esto no es correcto por 2 razones: 1) una clase abstracta puede tener métodos virtuales regulares además de los métodos virtuales puros, y 2) los métodos virtuales puros pueden tener opcionalmente una definición que se puede llamar con un nombre completo.
Michael Burr

Bien, pensándolo bien, imagino que si todos los métodos virtuales fueran puramente virtuales, el compilador podría optimizar la vtable (necesitaría ayuda para formar el enlazador para garantizar que no hubiera definiciones también).
Michael Burr

1
" La respuesta es que no se crea ninguna tabla virtual para las clases abstractas ". Incorrecto. "¡ No es necesario ya que no se pueden crear objetos de estas clases! " Incorrecto.
curioso

Puedo seguir su razón de ser de que no B debería ser necesaria ninguna tabla . El hecho de que algunos de sus métodos tengan implementaciones (predeterminadas) no significa que deban almacenarse en un vtable. Pero acabo de ejecutar su código (módulo algunas correcciones para compilarlo) gcc -Sseguido de c++filty claramente hay una vtable para Bincluida allí. Supongo que eso podría deberse a que vtable también almacena datos RTTI como nombres de clases y herencia. Podría ser necesario para un dynamic_cast<B*>. Incluso -fno-rttino hace que la vtable desaparezca. Con en clang -O3lugar de gccdesaparecer de repente.
MvG

@MvG "El hecho de que algunos de sus métodos tengan implementaciones (predeterminadas) no significa que deban almacenarse en una tabla virtual " Sí, eso significa exactamente eso.
curioso

0

muy linda prueba de concepto que hice un poco antes (para ver si el orden de herencia importa); avíseme si su implementación de C ++ realmente lo rechaza (mi versión de gcc solo da una advertencia para asignar estructuras anónimas, pero eso es un error), tengo curiosidad.

CCPolite.h :

#ifndef CCPOLITE_H
#define CCPOLITE_H

/* the vtable or interface */
typedef struct {
    void (*Greet)(void *);
    void (*Thank)(void *);
} ICCPolite;

/**
 * the actual "object" literal as C++ sees it; public variables be here too 
 * all CPolite objects use(are instances of) this struct's structure.
 */
typedef struct {
    ICCPolite *vtbl;
} CPolite;

#endif /* CCPOLITE_H */

CCPolite_constructor.h :

/** 
 * unconventionally include me after defining OBJECT_NAME to automate
 * static(allocation-less) construction.
 *
 * note: I assume CPOLITE_H is included; since if I use anonymous structs
 *     for each object, they become incompatible and cause compile time errors
 *     when trying to do stuff like assign, or pass functions.
 *     this is similar to how you can't pass void * to windows functions that
 *         take handles; these handles use anonymous structs to make 
 *         HWND/HANDLE/HINSTANCE/void*/etc not automatically convertible, and
 *         require a cast.
 */
#ifndef OBJECT_NAME
    #error CCPolite> constructor requires object name.
#endif

CPolite OBJECT_NAME = {
    &CCPolite_Vtbl
};

/* ensure no global scope pollution */
#undef OBJECT_NAME

main.c :

#include <stdio.h>
#include "CCPolite.h"

// | A Greeter is capable of greeting; nothing else.
struct IGreeter
{
    virtual void Greet() = 0;
};

// | A Thanker is capable of thanking; nothing else.
struct IThanker
{
    virtual void Thank() = 0;
};

// | A Polite is something that implements both IGreeter and IThanker
// | Note that order of implementation DOES MATTER.
struct IPolite1 : public IGreeter, public IThanker{};
struct IPolite2 : public IThanker, public IGreeter{};

// | implementation if IPolite1; implements IGreeter BEFORE IThanker
struct CPolite1 : public IPolite1
{
    void Greet()
    {
        puts("hello!");
    }

    void Thank()
    {
        puts("thank you!");
    }
};

// | implementation if IPolite1; implements IThanker BEFORE IGreeter
struct CPolite2 : public IPolite2
{
    void Greet()
    {
        puts("hi!");
    }

    void Thank()
    {
        puts("ty!");
    }
};

// | imposter Polite's Greet implementation.
static void CCPolite_Greet(void *)
{
    puts("HI I AM C!!!!");
}

// | imposter Polite's Thank implementation.
static void CCPolite_Thank(void *)
{
    puts("THANK YOU, I AM C!!");
}

// | vtable of the imposter Polite.
ICCPolite CCPolite_Vtbl = {
    CCPolite_Thank,
    CCPolite_Greet    
};

CPolite CCPoliteObj = {
    &CCPolite_Vtbl
};

int main(int argc, char **argv)
{
    puts("\npart 1");
    CPolite1 o1;
    o1.Greet();
    o1.Thank();

    puts("\npart 2");    
    CPolite2 o2;    
    o2.Greet();
    o2.Thank();    

    puts("\npart 3");    
    CPolite1 *not1 = (CPolite1 *)&o2;
    CPolite2 *not2 = (CPolite2 *)&o1;
    not1->Greet();
    not1->Thank();
    not2->Greet();
    not2->Thank();

    puts("\npart 4");        
    CPolite1 *fake = (CPolite1 *)&CCPoliteObj;
    fake->Thank();
    fake->Greet();

    puts("\npart 5");        
    CPolite2 *fake2 = (CPolite2 *)fake;
    fake2->Thank();
    fake2->Greet();

    puts("\npart 6");        
    #define OBJECT_NAME fake3
    #include "CCPolite_constructor.h"
    fake = (CPolite1 *)&fake3;
    fake->Thank();
    fake->Greet();

    puts("\npart 7");        
    #define OBJECT_NAME fake4
    #include "CCPolite_constructor.h"
    fake2 = (CPolite2 *)&fake4;
    fake2->Thank();
    fake2->Greet();    

    return 0;
}

salida:

part 1
hello!
thank you!

part 2
hi!
ty!

part 3
ty!
hi!
thank you!
hello!

part 4
HI I AM C!!!!
THANK YOU, I AM C!!

part 5
THANK YOU, I AM C!!
HI I AM C!!!!

part 6
HI I AM C!!!!
THANK YOU, I AM C!!

part 7
THANK YOU, I AM C!!
HI I AM C!!!!

tenga en cuenta que, dado que nunca estoy asignando mi objeto falso, no hay necesidad de hacer ninguna destrucción; los destructores se colocan automáticamente al final del alcance de los objetos asignados dinámicamente para recuperar la memoria del objeto literal y el puntero vtable.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.