virtuales que puro puras polimorfismo objeto modificador herencia funciones entre ejercicios diferencia c++ polymorphism virtual-functions vtable

c++ - que - ¿Cómo se implementan las funciones virtuales y vtable?



polimorfismo puro c++ (11)

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

De "Funciones virtuales en C ++"

Cada vez que un programa tiene una función virtual declarada, av - table se construye para la clase. La tabla v consiste en direcciones para las funciones virtuales para las 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 principio del objeto en la memoria. Por lo tanto, el tamaño del objeto en este caso aumenta según 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 clase, es decir, que solo hay una tabla virtual para una clase, independientemente del número de funciones virtuales que contenga. 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 usa 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 el vtable o incluso acceder directamente en tiempo de ejecución?

Universalmente, creo que la respuesta es "no". Podrías hacer algunos cambios en la memoria para encontrar el vtable, pero aún no sabrías cómo se ve la firma de la función para llamarlo. Cualquier cosa que desee lograr con esta capacidad (que el idioma admite) debería ser posible sin acceder directamente a la tabla de contenido o modificarla en tiempo de ejecución. También tenga en cuenta que la especificación del lenguaje C ++ no especifica que se requieren tablas virtuales, sin embargo, así es como la mayoría de los compiladores implementan funciones virtuales.

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

Creo que la respuesta aquí es "depende de la implementación" ya que la especificación no requiere tablas en primer lugar. Sin embargo, en la práctica, creo que todos los compiladores modernos solo crean un vtable si una clase tiene al menos 1 función virtual. Hay una sobrecarga de espacio asociada con el 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 resulta en un comportamiento indefinido si no está definido (lo que generalmente no es) (ISO / IEC 14882: 2003 10.4-2). En la práctica, asigna una ranura en el vtable para la función pero no le asigna una dirección. Esto deja incompleto el vtable que requiere que las clases derivadas implementen la función y completen el vtable. Algunas implementaciones simplemente colocan un puntero NULL en la entrada de 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 puede invocarse con una sintaxis id calificada (es decir, especificando por completo la clase en el nombre del método, similar a llamar a un método de clase base desde clase derivada). Esto se hace para proporcionar una implementación predeterminada fácil de usar, mientras que todavía se requiere que una clase derivada proporcione una anulación.

¿Tener una única 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, entonces alguien me ayude aquí si estoy equivocado.

Creo que solo las funciones que son virtuales en la clase experimentan el golpe de rendimiento de tiempo relacionado con llamar a una función virtual vs. una función no virtual. El espacio sobrecargado para la clase está allí de cualquier manera. Tenga en cuenta que si hay un vtable, solo hay 1 por clase , no uno por objeto .

¿La velocidad se ve afectada si la función virtual está anulada o no, o esto no tiene 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 llamar a la función virtual base. Sin embargo, hay una sobrecarga de espacio adicional para la clase asociada con la definición de otro 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 regreso)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/cxx-abi/abi.html#vtable

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

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

¿Existe el 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 única función virtual ralentiza 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, siempre que sea virtual.


¿Tener una única función virtual ralentiza 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, siempre que sea virtual.

Tener funciones virtuales ralentiza toda la clase en la medida en que un elemento más de datos tiene que ser inicializado, copiado, ... cuando se trata de un objeto de dicha clase. Para una clase con media docena de miembros más o menos, la diferencia debería ser insignificante. Para una clase que solo contenga un solo miembro char , o ningún miembro en absoluto, 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 de 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 si así 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 a un objeto de alguna clase derivada, necesita la ruta indirecta de vtable y la paga 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 del vtable se lee del objeto, el puntero a la función recuperado de la ranura apropiada y la función llamada por el puntero. En términos de rendimiento real, las predicciones de las sucursal podrían 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 pronostique correctamente a qué función llamar incluso antes de que se haya recuperado el puntero. Pero no importa cuál función es la común: podría ser la mayoría de los objetos que deleguen en el caso base no sobrescrito, o la mayoría de los objetos que pertenecen a la misma subclase y, por lo tanto, deleguen en el mismo caso sobrescrito.

¿Cómo se implementan en 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, para que el nivel bajo se vea más fácilmente.

clase de padres 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! }

clase derivada Bar

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 realizar 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, un vtable es solo un bloque estático en la memoria, que en su mayoría contiene punteros a funciones. Cada objeto de una clase polimórfica apuntará al 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 observando en qué vtable apunta. Lo anterior se simplifica de muchas maneras, como por ejemplo la herencia múltiple, pero el concepto general es el sonido.

Si arg es de tipo Foo* y toma arg->vtable , pero en realidad es un objeto de tipo Bar , entonces todavía obtiene la dirección correcta de la vtable de vtable . Esto se debe a que el vtable siempre es el primer elemento en la dirección del objeto, sin importar si se llama vtable o base.vtable en una expresión correctamente base.vtable .


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


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


Esta respuesta se ha incorporado a la respuesta Wiki de la comunidad

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

La respuesta es que no se especifica: llamar a la función virtual pura da como resultado un comportamiento indefinido si no está definido (lo que generalmente no es) (ISO / IEC 14882: 2003 10.4-2). Algunas implementaciones simplemente colocan un puntero NULL en la entrada de 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 puede invocarse con una sintaxis id calificada (es decir, especificando por completo la clase en el nombre del método, similar a llamar a un método de clase base desde clase derivada). Esto se hace para proporcionar una implementación predeterminada fácil de usar, mientras que todavía se requiere que una clase derivada proporcione una anulación.


Las respuestas de Burly son correctas aquí a excepción de 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 vblbl 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 vblbl 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 se reemplaza por D y una barra de método virtual () que se reemplaza, entonces D''s vtbl tendrá un puntero a foo () de B y a su propia barra () . Todavía no hay vtbl creado para B.


Por lo general, con una tabla VTable, una serie de indicadores para las funciones.


Puede volver a crear la funcionalidad de las funciones virtuales en C ++ usando punteros de función como miembros de una clase y funciones estáticas como las implementaciones, o usando el puntero a funciones de miembro y funciones de 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. De hecho, la herencia es solo una conveniencia de la nota ... todo puede implementarse sin usar las características del lenguaje para la herencia. :)

El siguiente es una mierda sin probar, probablemente con errores de código, 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; } ... };


Trataré de hacerlo simple :)

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

Esta es una matriz con punteros a funciones, que son implementaciones de una función virtual 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 el vtable con la clase base.
  • La clase derivada agrega y anula los métodos virtuales. En este caso, obtiene su propio vtable, donde las funciones virtuales agregadas tienen un índice que comienza después del último derivado.
  • Múltiples clases polimórficas en la herencia. En este caso, tenemos un cambio de índice entre la segunda y la siguiente base y su índice en la clase derivada

¿Se puede modificar el 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 ellas, pero eso puede ser solo una extensión.

¿Existe el 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 (incluso destructor) o derivan al menos una clase que tiene su variable vtable ("is polymorphic").

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

Esa es una posible implementación, pero no practicada. En cambio, usualmente hay una función que imprime algo así como "función virtual pura llamada" y abort() . La llamada a eso puede ocurrir si intenta llamar al método abstracto en el constructor o destructor.

¿Tener una única función virtual ralentiza 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, siempre que sea virtual.

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

Si llama a una función virtual a través de un puntero o referencia a un objeto, entonces siempre se implementará como llamada virtual, porque el compilador nunca podrá saber qué tipo de objeto se asignará a este puntero en tiempo de ejecución, y si es de un clase en la cual este método es anulado 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 sobre la clase real del objeto y puede "resolverlo" en tiempo de compilación. .
  • Si el método virtual se declara final en la clase a la que tiene un puntero o referencia a través del cual lo llama ( solo en C ++ 11 ). En este caso, el compilador sabe que este método no puede someterse a ningún reemplazo adicional y solo puede ser el método de esta clase.

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


muy linda prueba de concepto que hice un poco antes (para ver si importa el orden de la herencia); 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 ya que nunca estoy asignando mi objeto falso, no hay necesidad de hacer ninguna destrucción; los destructores se ponen automáticamente al final del alcance de los objetos asignados dinámicamente para reclamar la memoria de la misma literal del objeto y el puntero vtable.


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

No transportable, pero si no te molestan los trucos sucios, ¡seguro!

ADVERTENCIA : esta técnica no está recomendada para niños, adultos menores de 969 o pequeñas criaturas peludas de Alpha Centauri. Los efectos secundarios pueden incluir demonios que salen volando de tu nariz , la apariencia abrupta de Yog-Sothoth como un aprobador requerido en todas las revisiones posteriores del código, 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 los contenidos vtbl son simplemente una matriz de punteros de miembros allí (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 generalmente he observado.

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 tirar 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 (monoparqueando una clase)

Este es un poco más complicado, ya que el mismo vtbl está probablemente 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

Lo más probable es que este último haga que los verificadores de virus y el enlace se despierten y tomen nota, debido a las manipulaciones mprotect. En un proceso que usa el bit NX, puede fallar.