Saltar al contenido

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

Solución:

¿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, av – table se construye 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 lo 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 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 utiliza para resolver la llamada a la 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 acceder a la tabla vtable directamente o sin modificarla en tiempo de ejecución. También tenga en cuenta, la especificación del lenguaje C ++ no especificar que se requieren 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?

I creer 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!

I creer que solo las funciones que son virtuales en la clase experimentan el impacto de desempeño en el tiempo relacionado con llamar a una función virtual frente a una función no virtual. La sobrecarga de 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 regreso)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/cxx-abi/abi.html#vtable

  • ¿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 en general.

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 vtbl en sí mismo 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 bastante 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.

¿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 elemento más de datos tiene que inicializarse, copiarse, … cuando se trata de un objeto de dicha clase. Para una clase con media docena de miembros aproximadamente, la diferencia debería ser insignificante. Para una clase que solo contiene un char miembro, 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 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 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 del objeto, el puntero de función se recupera de la ranura correspondiente y la función se llama mediante el 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 pertenecientes 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 de qué tipo es una clase simplemente mirando a qué vtable apunta. Lo anterior está simplificado de muchas formas, como por ejemplo, herencia múltiple, pero el concepto general es sólido.

Si arg es de tipo Foo* y tu tomas arg->vtable, pero en realidad es un objeto de tipo Bar, entonces todavía obtienes la dirección correcta del vtable. Eso es porque el vtable es siempre el primer elemento en la dirección del objeto, sin importar si se llama vtable o base.vtable en una expresión escrita correctamente.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *