2009-03-21 11 views
6

He encontrado lo que parece ser un error realmente molesto ejecutar mi programa C++ bajo Microsoft Visual C++ 2003, pero podría ser algo que estoy haciendo mal, así que pensé Lo lanzaría aquí y vería si alguien tiene alguna idea.C++ "esto" no coincide con el método de objeto fue llamado en

que tienen una jerarquía de clases de este tipo (tal y como es - por ejemplo, no hay herencia múltiple en el código real):

class CWaitable 
{ 
public: 
    void WakeWaiters() const 
    { 
     CDifferentClass::Get()->DoStuff(this); // Breakpoint here 
    } 
}; 

class CMotion : public CWaitable 
{ 
    virtual void NotUsedInThisExampleButPertinentBecauseItsVirtual() { } 
}; 

class CMotionWalk : public CMotion 
{ ... }; 

void AnnoyingFunctionThatBreaks(CMotion* pMotion) 
{ 
    pMotion->WakeWaiters(); 
} 

bien, así que llaman "AnnoyingFunctionThatBreaks" con una instancia de "CMotionWalk" (por ejemplo, el depurador dice que es 0x06716fe0), y todo parece estar bien. Pero cuando entro en él, al punto de interrupción en la llamada a "DoStuff", el puntero "this" tiene un valor diferente al puntero pMotion al que puse el método (por ejemplo, ahora el depurador dice una palabra más arriba - 0x06716fe4).

Para expresarlo de manera diferente: pMotion tiene el valor 0x06716fe0, pero cuando invoco un método en él, ese método ve 'esto' como 0x06716fe4.

No me estoy volviendo loco, ¿verdad? Eso es raro, ¿verdad?

+0

El corte ocurre en objetos y no en punteros. El código que publicaste funciona después de un pequeño arreglo. Publica un código real: mi bola de crystall no funciona hoy. Por cierto: realmente no tienes la intención de pasar 'esto', ¿o sí? – dirkgently

+0

Es casi seguro que se trata de una herencia múltiple; es probable que la muestra del código haya sido recortada en exceso. –

+0

@Earwicker: ¿De dónde obtuviste la herencia múltiple? – dirkgently

Respuesta

10

Creo que simplemente está viendo un artefacto de la forma en que el compilador está construyendo los vtables. Sospecho que CMotion tiene funciones virtuales propias y, por lo tanto, terminas con desplazamientos dentro del objeto derivado para llegar al objeto base. Por lo tanto, diferentes punteros.

Si está funcionando (es decir, si esto no produce bloqueos, y no hay punteros fuera de los objetos) entonces no me preocuparía demasiado.

+0

Bueno, no funciona, porque el método DoStuff se confunde si "esto" no es igual a lo que se ha visto antes. Pero está al tanto con su explicación: la agregaré a la pregunta de que CMotion tiene funciones virtuales, por lo que declarar CWaitable :: WakeWaiters como virtual resuelve el problema. – andygeers

+0

no entiendo cómo podría cambiar la compensación. la clase derivada tiene una clase base y ambas comienzan en el mismo lugar en la memoria. ¿por qué en la tierra debería ser diferente la compensación sin herencia múltiple? ¿msvc hace algo extraño? –

+0

No tengo idea de cómo MSVC configura las cosas en la memoria, pero si lo piensas bien, tiene que haber algún lugar dentro del objeto Derivado donde se inicia el objeto Base. Es posible poner los elementos derivados ANTES o DESPUÉS de los elementos base, según el capricho del compilador. –

2

Véase también wikipedia article on thunking. Si configura el depurador para pasar por el código de ensamblado, debería verlo suceder. (ya sea un truco o simplemente cambiar el offset depende de los detalles que ha elidido del código que usted da)

+0

Intenté recorrer el código de ensamblaje. Realmente no sé lo que estoy buscando, pero no se veía tan inusual – andygeers

0

Necesita publicar un código real. Los valores de los punteros en los siguientes son los esperados - es decir, que son los mismos:

#include <iostream> 
using namespace std; 

struct A { 
    char x[100]; 
    void pt() { 
     cout << "In A::pt this = " << this << endl; 
    } 
}; 

struct B : public A { 
    char z[100]; 
}; 

void f(A * a) { 
    cout << "In f ptr = " << a << endl; 
    a->pt(); 
} 

int main() { 
    B b; 
    f(&b); 
} 
6

es de clase CMotion está derivando alguna otra clase también que contiene una función virtual? He encontrado que el este puntero no cambia con el código que envió, sin embargo, cambia si tiene ese algo jerarquía de la siguiente manera:

class Test 
{ 
public: 
    virtual void f() 
    { 

    } 
}; 

class CWaitable 
{ 
public: 
    void WakeWaiters() const 
    { 
     const CWaitable* p = this; 
    } 
}; 

class CMotion : public CWaitable, Test 
{ }; 


class CMotionWalk : public CMotion 
{ 
public: 
}; 



void AnnoyingFunctionThatBreaks(CMotion* pMotion) 
{ 
    pMotion->WakeWaiters(); 
} 

Creo que esto es debido a la herencia múltiple para la clase CMotion y el puntero vtable en CMotion que apunta a Test :: f()

+0

Definitivamente no hay herencia múltiple. – andygeers

0

No puedo explicar por qué esto funciona, pero se declara CWaitable :: WakeWaiters como correcciones de la cuestión virtuales

1

Creo que puedo explicar esto ... hay una mejor explicación alguna parte de uno de cualquiera de Meyer o los libros de Sutter, pero no tenía ganas de buscar. Creo que lo que estás viendo es una consecuencia de cómo se implementan las funciones virtuales (vtables) y la naturaleza de "C++ no pagas hasta que lo usas".

Si no hay métodos virtuales en uso, un puntero al objeto apunta a los datos del objeto. Tan pronto como se introduce un método virtual, el compilador inserta una tabla de búsqueda virtual (vtable) y el puntero apunta a esto en su lugar. Probablemente me esté perdiendo algo (y mi cerebro aún no funciona) ya que no pude lograr que esto ocurriera hasta que inserté un miembro de datos en la clase base. Si la clase base tiene un miembro de datos y la primera clase hija tiene una virtual, entonces los desplazamientos difieren según el tamaño de la tabla vtable (4 en mi compilador).Aquí hay un ejemplo que muestra esto claramente:

template <typename T> 
void displayAddress(char const* meth, T const* ptr) { 
    std::printf("%s - this = %08lx\n", static_cast<unsigned long>(ptr)); 
    std::printf("%s - typeid(T).name() %s\n", typeid(T).name()); 
    std::printf("%s - typeid(*ptr).name() %s\n", typeid(*ptr).name()); 
} 

struct A { 
    char byte; 
    void f() { displayAddress("A::f", this); } 
}; 
struct B: A { 
    virtual void v() { displayAddress("B::v", this); } 
    virtual void x() { displayAddress("B::x", this); } 
}; 
struct C: B { 
    virtual void v() { displayAddress("C::v", this); } 
}; 

int main() { 
    A aObj; 
    B bObj; 
    C cObj; 

    std::printf("aObj:\n"); 
    aObj.f(); 

    std::printf("\nbObj:\n"); 
    bObj.f(); 
    bObj.v(); 
    bObj.x(); 

    std::printf("\ncObj:\n"); 
    cObj.f(); 
    cObj.v(); 
    cObj.x(); 

    return 0; 
} 

La ejecución de este en mi máquina (MacBook Pro) imprime la siguiente:

aObj: 
A::f - this = bffff93f 
A::f - typeid(T)::name() = 1A 
A::f - typeid(*ptr)::name() = 1A 

bObj: 
A::f - this = bffff938 
A::f - typeid(T)::name() = 1A 
A::f - typeid(*ptr)::name() = 1A 
B::v - this = bffff934 
B::v - typeid(T)::name() = 1B 
B::v - typeid(*ptr)::name() = 1B 
B::x - this = bffff934 
B::x - typeid(T)::name() = 1B 
B::x - typeid(*ptr)::name() = 1B 

cObj: 
A::f - this = bffff930 
A::f - typeid(T)::name() = 1A 
A::f - typeid(*ptr)::name() = 1A 
C::v - this = bffff92c 
C::v - typeid(T)::name() = 1C 
C::v - typeid(*ptr)::name() = 1C 
B::x - this = bffff92c 
B::x - typeid(T)::name() = 1B 
B::x - typeid(*ptr)::name() = 1C 

Lo interesante es que tanto bObj y cObj exhiben el cambio de dirección entre llamando a los métodos A y B o C. La diferencia es que B contiene un método virtual. Esto permite al compilador insertar la tabla adicional necesaria para implementar la virtualización de funciones. La otra cosa interesante que muestra este programa es que typeid(T) y typeid(*ptr) es diferente en B::x cuando se llama virtualmente. También puede ver un aumento de tamaño usando sizeof tan pronto como se inserta la tabla virtual.

En su caso, tan pronto como haya creado CWaitable::WakeWaiters virtual, se inserta el vtable y realmente presta atención al tipo real del objeto, así como a la inserción de las estructuras de contabilidad necesarias. Esto hace que el desplazamiento a la base del objeto sea diferente. Realmente me gustaría poder encontrar la referencia que describe un diseño de memoria mítico y por qué la dirección de un objeto depende del tipo que se está interpretando como cuando la herencia se mezcla en la diversión.

Regla general: (y usted ha escuchado esto antes) clases base siempre tienen destructores virtuales. Esto ayudará a eliminar pequeñas sorpresas como esta.

+0

La idea es que cada clase tenga sus datos almacenados en un bloque contiguo de memoria y sus métodos se generen de tal manera que el acceso a esos datos sea lo más rápido posible. Por lo tanto, el puntero "this" para esos métodos no tiene que cambiar dentro del mismo contexto. –

Cuestiones relacionadas