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?
c++
polymorphism
virtual-functions
vtable
Brian R. Bondy
fuente
fuente
Inside the C++ Object Model
porStanley B. Lippman
. (Sección 4.2, página 124-131)Respuestas:
¿Cómo se implementan las funciones virtuales a un nivel profundo?
De "Funciones virtuales en C ++" :
¿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
fuente
No portátil, pero si no te importan los trucos sucios, ¡seguro!
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.
Ahora para hacer algunas travesuras ...
Cambio de clase en tiempo de ejecución:
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.
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.
fuente
¿Tener una sola función virtual ralentiza a toda la clase?
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
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 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.
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
barra de clase derivada
función f realizando una llamada de función virtual
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
arg
es de tipoFoo*
y lo tomasarg->vtable
, pero en realidad es un objeto de tipoBar
, obtendrás la dirección correcta delvtable
. Eso es porquevtable
siempre es el primer elemento en la dirección del objeto, sin importar si se llamavtable
obase.vtable
en una expresión escrita correctamente.fuente
Normalmente con VTable, una matriz de punteros a funciones.
fuente
Esta respuesta se ha incorporado al respuesta de Community Wiki
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.
fuente
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
fuente
void(*)(Foo*) MyFunc;
¿Es esto alguna sintaxis de Java?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:
¿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:
final
en 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 comobool Horse::HasHoof() { return true; }
le proporcionaría la capacidad de llamarif (anim->HasHoof())
que será más rápido que intentarloif(dynamic_cast<Horse*>(anim))
. Esto se debe a quedynamic_cast
tiene 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.fuente
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.fuente
Cada objeto tiene un puntero vtable que apunta a una matriz de funciones miembro.
fuente
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.
fuente
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:
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.
fuente
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 -S
seguido dec++filt
y claramente hay una vtable paraB
incluida 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 undynamic_cast<B*>
. Incluso-fno-rtti
no hace que la vtable desaparezca. Con enclang -O3
lugar degcc
desaparecer de repente.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 :
CCPolite_constructor.h :
main.c :
salida:
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.
fuente