¿Qué es un vtable
?
Puede ser útil saber de qué está hablando el mensaje de error antes de intentar solucionarlo. Comenzaré en un nivel alto, luego bajaré a algunos detalles más. De esa manera, las personas pueden saltar una vez que se sientan cómodos con su comprensión de las vtables. ... y hay un montón de gente saltando por delante en este momento. :) Para aquellos que se quedan:
Una vtable es básicamente la implementación más común del polimorfismo en C ++ . Cuando se usan vtables, cada clase polimórfica tiene una vtable en algún lugar del programa; Puedes considerarlo como un static
miembro de datos (oculto) de la clase. Cada objeto de una clase polimórfica está asociado con la tabla vtable para su clase más derivada. Al marcar esta asociación, el programa puede hacer funcionar su magia polimórfica. Advertencia importante: una vtable es un detalle de implementación. No es un mandato del estándar C ++, aunque la mayoría (¿todos?) De los compiladores C ++ usan vtables para implementar el comportamiento polimórfico. Los detalles que presento son enfoques típicos o razonables. ¡Los compiladores pueden desviarse de esto!
Cada objeto polimórfico tiene un puntero (oculto) a la tabla v para la clase más derivada del objeto (posiblemente múltiples punteros, en los casos más complejos). Al observar el puntero, el programa puede determinar cuál es el tipo "real" de un objeto (excepto durante la construcción, pero omita ese caso especial). Por ejemplo, si un objeto de tipo A
no apunta a la tabla de A
, entonces ese objeto es en realidad un sub-objeto de algo derivado de A
.
El nombre "vtable" viene de " v función irtual mesa ". Es una tabla que almacena punteros a funciones (virtuales). Un compilador elige su convención para la disposición de la tabla; un enfoque simple es pasar por las funciones virtuales en el orden en que se declaran dentro de las definiciones de clase. Cuando se llama a una función virtual, el programa sigue el puntero del objeto a una tabla v, va a la entrada asociada con la función deseada y luego usa el puntero de función almacenado para invocar la función correcta. Hay varios trucos para hacer que esto funcione, pero no los abordaré aquí.
¿Dónde / cuándo se vtable
genera un ?
El compilador genera automáticamente una vtable (a veces llamada "emitida"). Un compilador podría emitir una vtable en cada unidad de traducción que vea una definición de clase polimórfica, pero eso generalmente sería una exageración innecesaria. Una alternativa ( utilizada por gcc , y probablemente por otros) es elegir una sola unidad de traducción en la que colocar la vtable, similar a cómo elegiría un único archivo fuente en el que colocar los miembros de datos estáticos de una clase. Si este proceso de selección no puede seleccionar ninguna unidad de traducción, entonces la tabla vtable se convierte en una referencia indefinida. De ahí el error, cuyo mensaje no es particularmente claro.
Del mismo modo, si el proceso de selección elige una unidad de traducción, pero ese archivo de objeto no se proporciona al vinculador, entonces la tabla vtable se convierte en una referencia indefinida. Desafortunadamente, el mensaje de error puede ser aún menos claro en este caso que en el caso en que el proceso de selección falló. (Gracias a los que respondieron que mencionaron esta posibilidad. Probablemente lo habría olvidado de otra manera).
El proceso de selección utilizado por gcc tiene sentido si comenzamos con la tradición de dedicar un archivo fuente (único) a cada clase que necesita uno para su implementación. Sería bueno emitir el vtable al compilar ese archivo fuente. Llamemos a eso nuestro objetivo. Sin embargo, el proceso de selección debe funcionar incluso si no se sigue esta tradición. Entonces, en lugar de buscar la implementación de toda la clase, busquemos la implementación de un miembro específico de la clase. Si se sigue la tradición, y si ese miembro se implementa de hecho , esto logra el objetivo.
El miembro seleccionado por gcc (y potencialmente por otros compiladores) es la primera función virtual no en línea que no es puramente virtual. Si eres parte de la multitud que declara constructores y destructores antes que otras funciones miembro, entonces ese destructor tiene una buena posibilidad de ser seleccionado. (Se acordó de hacer que el destructor sea virtual, ¿verdad?) Hay excepciones; Esperaría que las excepciones más comunes sean cuando se proporciona una definición en línea para el destructor y cuando se solicita el destructor predeterminado (usando " = default
").
El astuto podría notar que una clase polimórfica puede proporcionar definiciones en línea para todas sus funciones virtuales. ¿Eso no hace que el proceso de selección falle? Lo hace en compiladores más antiguos. He leído que los últimos compiladores han abordado esta situación, pero no sé los números de versión relevantes. Podría intentar buscar esto, pero es más fácil codificarlo o esperar a que el compilador se queje.
En resumen, hay tres causas clave del error "referencia indefinida a vtable":
- A una función miembro le falta su definición.
- Un archivo de objeto no se está vinculando.
- Todas las funciones virtuales tienen definiciones en línea.
Estas causas son en sí mismas insuficientes para causar el error por sí mismas. Más bien, esto es lo que abordaría para resolver el error. No espere que crear intencionalmente una de estas situaciones produzca definitivamente este error; Hay otros requisitos. Espere que resolver estas situaciones resuelva este error.
(OK, el número 3 podría haber sido suficiente cuando se hizo esta pregunta).
¿Cómo arreglar el error?
¡Bienvenido de nuevo a la gente que salta por delante! :)
- Mira la definición de tu clase. Encuentre la primera función virtual no en línea que no sea puramente virtual (no "
= 0
") y cuya definición proporcione (no " = default
").
- Si no existe tal función, intente modificar su clase para que haya una. (Error posiblemente resuelto).
- Vea también la respuesta de Philip Thomas para una advertencia.
- Encuentra la definición de esa función. Si falta, ¡agrégalo! (Error posiblemente resuelto).
- Verifique su comando de enlace. Si no menciona el archivo objeto con la definición de esa función, ¡corríjalo! (Error posiblemente resuelto).
- Repita los pasos 2 y 3 para cada función virtual, luego para cada función no virtual, hasta que se resuelva el error. Si todavía está atascado, repita para cada miembro de datos estáticos.
Ejemplo
Los detalles de qué hacer pueden variar y, a veces, ramificarse en preguntas separadas (como ¿Qué es una referencia indefinida / error de símbolo externo no resuelto y cómo lo soluciono? ). Sin embargo, proporcionaré un ejemplo de qué hacer en un caso específico que podría confundir a los programadores más nuevos.
El paso 1 menciona la modificación de su clase para que tenga una función de cierto tipo. Si la descripción de esa función se te pasó por alto, podrías estar en la situación que pretendo abordar. Tenga en cuenta que esta es una forma de lograr el objetivo; no es la única forma, y fácilmente podría haber mejores formas en su situación específica. Llamemos a tu clase A
. ¿Su destructor está declarado (en su definición de clase) como
virtual ~A() = default;
o
virtual ~A() {}
? Si es así, dos pasos cambiarán su destructor en el tipo de función que queremos. Primero, cambie esa línea a
virtual ~A();
Segundo, ponga la siguiente línea en un archivo fuente que sea parte de su proyecto (preferiblemente el archivo con la implementación de la clase, si tiene uno):
A::~A() {}
Eso hace que su destructor (virtual) no esté en línea y no sea generado por el compilador. (No dude en modificar las cosas para que coincidan mejor con su estilo de formato de código, como agregar un comentario de encabezado a la definición de la función).