2010-05-24 9 views
6

La situación está siguiendo. He compartido biblioteca, que contiene la definición de clase -¿Por qué la función virtual de C++ definida en el encabezado no se puede compilar y vincular en vtable?

QueueClass : IClassInterface 
{ 
    virtual void LOL() { do some magic} 
} 

Mi biblioteca compartida inicializar miembro de la clase

QueueClass *globalMember = new QueueClass(); 

mi biblioteca Compartir función de exportación C que devuelve puntero a globalMember -

void * getGlobalMember(void) { return globalMember;} 

Mi la aplicación usa globalMember como esta

((IClassInterface*)getGlobalMember())->LOL(); 

Ahora las cosas muy súper - si no hago referencia a LOL desde la biblioteca compartida, entonces LOL no está vinculado y llamar desde la aplicación genera una excepción. Motivo: VTABLE contiene nul en lugar del puntero a la función LOL().

Cuando muevo la definición de LOL() del archivo .h a .cpp, de repente aparece en VTABLE y todo funciona muy bien. ¿Qué explica este comportamiento? (compilador gcc + arquitectura ARM_)

+0

¿Qué pasa con cualquier otra función virtual no en línea en 'QueueClass' (si hay alguna)? ¿Funcionan? En otras palabras, ¿está el problema con 'LOL' local en la entrada de' 'VOL de' 'LOL'' o si la tabla completa está vacía o falta? – AnT

+0

¿por qué heredar de forma privada IClassInterface? –

+2

¿Por qué la función 'get ...' devuelve 'void *' en lugar de 'IClassInterface *'? – AnT

Respuesta

6

El enlazador es el culpable aquí. Cuando una función está en línea, tiene múltiples definiciones, una en cada archivo cpp a la que se hace referencia. Si su código nunca hace referencia a la función, nunca se genera.

Sin embargo, el diseño vtable se determina en tiempo de compilación con la definición de clase. El compilador puede decir fácilmente que el LOL() es una función virtual y necesita tener una entrada en el vtable.

Cuando llega el momento del enlace para la aplicación, intenta completar todos los valores de QueueClass::_VTABLE pero no encuentra una definición de LOL() y la deja en blanco (nulo).

La solución es hacer referencia a LOL() en un archivo en la biblioteca compartida. Algo tan simple como &QueueClass::LOL;. Es posible que deba asignarlo a una variable de descarte para que el compilador deje de quejarse de las declaraciones sin efecto.

+0

Esto significa que el compilador + vinculador son simplemente locos! Nunca deben permitirle crear un objeto vivo en tiempo de ejecución cuya tabla virtual no está completamente definida. En tal caso, el enlazador podría generar un error "no resuelto" – valdo

+0

El compilador solo está optimizando. Si nunca usa la función, ¿por qué debería construir una? Si el compilador emite código para cada función en línea para cada archivo, los tiempos de compilación serían enormes, los tiempos de enlace también serían enormes (el enlazador tendría que ordenar los reems de funciones duplicadas sin referencia). –

+0

Normalmente, el 'no resuelto' se genera cuando se carga la biblioteca, no cuando se crea, por lo que tiene la posibilidad de hacer referencia a los símbolos de otra biblioteca. –

1

Supongo que GCC está aprovechando la oportunidad para alinear la llamada a LOL. Voy a ver si puedo encontrar una referencia para usted en este ...

Veo sechastain me dio una descripción más detallada y no pude buscar en google la referencia que estaba buscando. Así que lo dejo así.

+0

Sí, si gcc no ve ninguna no-inline, funciones virtuales en una clase, no va a emitir la viable para ella . http://gcc.gnu.org/faq.html # vtables –

+0

Gracias, eso ayuda. Solo recuerdo haber leído un buen artículo sobre este tema, pero Teh Great Gizoogle me está fallando en este momento, ya que no puedo encontrar la combinación correcta de palabras clave para encontrarlo. Ya es suficientemente malo no puedo mantener las cosas en mi cabeza real, cuando mi cerebro virtual también tiene goteras, eso me deja en un estado lamentable ;-) – Ukko

+0

Whoohoo, también recibí mi primer neg por una respuesta con este ! Admito que no fue mi mejor trabajo, y para aquellos interesados ​​el +1 seguido por el -1 anotó a +8. Siempre me pregunté si funcionaría de esa manera o si perdería el +10 original, ahora lo sabemos ;-) – Ukko

0

Las funciones definidas en los archivos de encabezado están alineadas en el uso. No están compilados como parte de la biblioteca; en cambio, cuando se realiza la llamada, el código de la función simplemente reemplaza el código de la llamada, y eso es lo que se compila.

Por lo tanto, no me sorprende ver que no se encuentra una entrada en la tabla v (¿qué indicaría?), Y no me sorprende ver que la definición de la función se mueve a un archivo .cpp de repente hace que las cosas funcionen. Estoy un poco sorprendido de que crear una instancia del objeto con una llamada en la biblioteca haga la diferencia, sin embargo.

No estoy seguro de si es prisa de su parte, pero del código proporcionado IClassInterface no contiene necesariamente LOL, solo QueueClass. Pero está lanzando a un puntero IClassInterface para hacer la llamada LOL.

+3

Esto es un razonamiento falso. El hecho de que una función esté en línea no significa que no pueda tomar su dirección. "Lo que señalaría" es el problema del compilador y no el del usuario. Normalmente, los compiladores generan un cuerpo independiente para una función. Esto es lo que debería haber hecho en este caso también. En otras palabras, no hay problemas con el código/enfoque en sí mismo. – AnT

+0

Bueno, no es completamente falso. Cuando vuelves a C días poner código en un encabezado era una receta total para el desastre, las cosas son mejores ahora; sin embargo, todavía hay problemas con el lugar donde el compilador debe emitir ese código. Según su modelo, incluir un archivo de encabezado dará como resultado la emisión de una versión no en línea de muchos métodos, lo que generará enormes archivos de objetos y mucho trabajo de vinculación sin obtener ganancias. (Este es un gran problema cuando se incluyen plantillas). Todo esto proviene del problema raíz de la compilación separada basada en archivos. Se hicieron concesiones, lo siento por eso. – Ukko

+0

Me parece pasar por un 'void *' y dar vueltas a todo un problema, especialmente cuando se le da la cadena de conversión: 'QueueClass *' -> 'void *' -> 'IClassInterface *';) –

0

Si este ejemplo se simplifica y su árbol de herencia real usa herencia múltiple, esto podría explicarse fácilmente. Cuando realiza una conversión de tipo en un puntero de objeto, el compilador necesita ajustar el puntero para hacer referencia a la tabla de contenido adecuada. Como devuelve un void *, el compilador no tiene la información necesaria para realizar el ajuste.

Editar: No hay un estándar para el diseño de objetos C++, pero para un ejemplo de cómo la herencia múltiple podría funcionar ver este artículo desde el propio Bjarne Stroustrup: http://www-plan.cs.colorado.edu/diwan/class-papers/mi.pdf

Si este es su problema, que podría ser capaz de solucionar con un simple cambio:

IClassInterface *globalMember = new QueueClass(); 

el compilador de C++ hará las modificaciones necesarias puntero cuando se efectúa la cesión, por lo que la función C puede devolver el puntero correcto.

+0

¿Cómo funciona la herencia múltiple en ¿mundo real? ¿Cómo se crean los vtables? Say - class multiple: interface1, interface2; ¿Cómo se verá vtable este monstruo ??? Estoy realmente confundido. –

+0

@ 0xDEAD BEEF, mira mi actualización. –

2

No estoy de acuerdo con @sechastain.

Inline está lejos de ser automático. Independientemente de si el método se define o no en el lugar o se utiliza una sugerencia (inline palabra clave o __forceinline), el compilador es el único que decide si la alineación realmente tendrá lugar y usa heurística complicada para hacerlo. Sin embargo, un caso particular es que no debe alinear una llamada cuando se invoca un método virtual utilizando el despacho en tiempo de ejecución, precisamente porque el despacho en tiempo de ejecución y la alineación no son compatibles.

Para comprender la precisión de "utilizar el envío en tiempo de ejecución":

IClassInterface* i = /**/; 
i->LOL();     // runtime dispatch 
i->QueueClass::LOL();  // compile time dispatch, inline is possible 

@0xDEAD BEEF: Encuentro su diseño frágil para decir lo menos.

El uso de C-Style arroja aquí está mal:

QueueClass* p = /**/; 
IClassInterface* q = p; 

assert(((void*)p) == ((void*)q)); // may fire or not... 

Fundamentalmente no hay garantía de que las 2 direcciones son iguales: es la aplicación definida, y es improbable que se resisten al cambio.

les deseo ser capaz de lanzar con seguridad el puntero void* a un puntero IClassInterface* entonces usted necesita para crear desde una IClassInterface* originalmente para que el compilador de C++ puede realizar la aritmética de punteros correcta dependiendo de la disposición de los objetos.

Por supuesto, también destacaré que el uso de variables globales ... probablemente usted lo sepa.

¿Cuál es el motivo de la ausencia? Honestamente, no veo nada aparte de un error en el compilador/enlazador. He visto la definición en línea de virtual funciones varias veces (más específicamente, el método clone) y nunca causó problemas.

EDITAR: Ya que "la aritmética de punteros correcta" no fue tan bien entendido, aquí es un ejemplo

struct Base1 { char mDum1; }; 

struct Base2 { char mDum2; }; 

struct Derived: Base1, Base2 {}; 

int main(int argc, char* argv[]) 
{ 
    Derived d; 
    Base1* b1 = &d; 
    Base2* b2 = &d; 

    std::cout << "Base1: " << b1 
      << "\nBase2: " << b2 
      << "\nDerived: " << &d << std::endl; 

    return 0; 
} 

Y aquí es lo que estaba impreso:

Base1: 0x7fbfffee60 
Base2: 0x7fbfffee61 
Derived: 0x7fbfffee60 

No es la diferencia entre el valor de b2 y &d, aunque se refieran a una entidad. Esto se puede entender si uno piensa en el diseño de la memoria del objeto.

Derived 
Base1  Base2 
+-------+-------+ 
| mDum1 | mDum2 | 
+-------+-------+ 

Al convertir de Derived* a Base2*, el compilador llevará a cabo el ajuste necesario (en este caso, incremente la dirección del puntero por un byte) de manera que el puntero termina señalando de manera efectiva a la parte Base2 de Derived y no a la Base1 parte interpretada erróneamente como un objeto Base2 (que sería desagradable).

Es por esto que el uso de C-Style arroja que hay que evitar cuando downcasting. Aquí, si tiene un puntero Base2, no puede reinterpretarlo como un puntero Derived. En su lugar, deberá usar static_cast<Derived*>(b2), que disminuirá el puntero en un byte para que apunte correctamente al comienzo del objeto Derived.

punteros Manipular que normalmente se conoce como la aritmética de punteros. Aquí el compilador realizará automáticamente el ajuste correcto ... a condición de conocer el tipo.

Lamentablemente, el compilador no puede realizarlos al convertir desde un void*, por lo tanto, depende del desarrollador asegurarse de que maneja correctamente esto. La regla general es la siguiente: T* -> void* -> T* con el mismo tipo que aparece en ambos lados.

Por lo tanto, usted debe (simplemente) corregir su código declarando: IClassInterface* globalMember y que no tendría ningún problema de portabilidad. Probablemente aún tengas problemas de mantenimiento, pero ese es el problema de usar C con código OO: C no tiene conocimiento de que haya nada orientado a objetos.

+0

¡No entiendo por completo de qué estás hablando! :) PERO - 1) ¡Solo puedo tener void * en la función C exportada porque ES C la función exportada! ;) 2) es la primera vez que escucho sobre "el compilador de C++ puede realizar la aritmética correcta del puntero dependiendo del diseño de los objetos" ... ¿qué?:) –

+0

BTW - ¿Qué pasa con mi diseño? Bueno, me encanta C++, pero las bibliotecas compartidas solo permiten C funciones exportadas. ¿Que debería hacer entonces? Conviértete en un codificador de C cojo y renuncia a todo lo bueno que ofrecen los oradores de C++. :) –

+0

El problema es que usted supone que un objeto en C++ tiene una dirección única, independientemente de la forma en que se refiera a ella. Desafortunadamente, este no es el caso. Editaré mi publicación para agregar un pequeño ejemplo con valores del mundo real para ilustrarlo. –

Cuestiones relacionadas