2009-03-21 6 views
6

Mi sono imbattuto in quello che sembra un bug davvero fastidioso che esegue il mio programma C++ in Microsoft Visual C++ 2003, ma potrebbe essere solo qualcosa che sto facendo male Lo butterei qui e vedrei se qualcuno ha qualche idea.C++ "questo" non corrisponde al metodo dell'oggetto è stato chiamato su

Ho una gerarchia di classi come questo (esattamente come è - per esempio non c'è ereditarietà multipla nel codice reale):

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(); 
} 

Va bene, così io chiamo "AnnoyingFunctionThatBreaks" con un'istanza "CMotionWalk" (ad esempio il debugger dice che è 0x06716fe0), e tutto sembra a posto. Ma quando ci passo, al punto di interruzione della chiamata a "DoStuff", il puntatore "this" ha un valore diverso rispetto al puntatore pMotion su cui ho chiamato il metodo (ad esempio, il debugger dice una parola più alta - 0x06716fe4).

Per esprimerlo in modo diverso: pMotion ha il valore 0x06716fe0, ma quando chiamo un metodo su di esso, quel metodo vede 'questo' come 0x06716fe4.

Non sto diventando matto, vero? È strano, vero?

+0

Il taglio avviene sugli oggetti e non sui puntatori. Il codice che hai postato funziona dopo un piccolo aggiustamento. Pubblica un codice reale - la mia sfera di cristallo non funziona oggi. A proposito: non hai intenzione di passare "questo" in giro, vero? – dirkgently

+0

Questa è quasi certamente un'eredità multipla: l'esempio di codice è stato probabilmente sovradimensionato. –

+0

@Earwicker: Da dove hai ereditato multipli? – dirkgently

risposta

10

Credo che stai semplicemente vedendo un artefatto del modo in cui il compilatore sta costruendo i vtables. Sospetto che CMotion abbia funzioni virtuali di sua proprietà, e quindi si finisce con gli offset all'interno dell'oggetto derivato per arrivare all'oggetto base. Quindi, diversi indicatori.

Se funziona (vale a dire se questo non produce arresti anomali e non ci sono puntatori all'esterno degli oggetti), non mi preoccuperei troppo di ciò.

+0

Beh, non funziona, perché il metodo DoStuff si confonde se "questo" non è lo stesso di quello visto prima. Ma sei esattamente nella tua spiegazione - aggiungerò alla domanda, che CMotion ha funzioni virtuali, quindi dichiarare CWaitable :: WakeWaiters come virtuale risolve il problema. – andygeers

+0

non capisco come lo spostamento potrebbe cambiare. la classe derivata ha una classe base e entrambi iniziano nello stesso punto in memoria. perché mai l'offset dovrebbe essere diverso senza ereditarietà multipla? msvc fa qualcosa di strano? –

+0

Non ho idea di come MSVC metta le cose in memoria, ma se ci pensate, ci deve essere da qualche parte all'interno dell'oggetto Derived dove inizia l'oggetto Base. È possibile inserire gli elementi derivati ​​PRIMA o DOPO gli elementi di base, a seconda del capriccio del compilatore. –

2

Vedere anche wikipedia article on thunking. Se si imposta il debugger per scorrere il codice assembly, dovresti vederlo accadere. (se si tratta di un thunk o semplicemente la modifica dell'offset dipende dai dettagli che hai elidato dal codice che dai)

+0

Ho provato a passare attraverso il codice assembly. Non so davvero cosa sto cercando, ma non sembrava così insolito. – andygeers

0

È necessario inserire del codice effettivo. I valori per i puntatori nel seguente sono come previsto - vale a dire che sono la stessa cosa:

#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

è di classe cmotion ne trae qualche altra classe anche che contiene una funzione virtuale? Ho trovato che il questo puntatore non cambia con il codice che hai postato, ma cambia se avete la qualcosa gerarchia simile a questo:

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(); 
} 

Credo che questo sia a causa della ereditarietà multipla per la classe cmotion e il puntatore vtable in CMotion che punta a Test :: f()

+0

Non c'è sicuramente alcuna eredità multipla. – andygeers

0

non riesco a spiegare il motivo per cui questo funziona, ma dichiarano CWaitable :: WakeWaiters esempio correzioni virtuali la questione

1

penso che posso spiegare questo ... c'è una spiegazione migliore da qualche parte in una delle due Meyer oi libri di Sutter, ma non avevo voglia di cercare. Credo che ciò che state vedendo sia una conseguenza del modo in cui le funzioni virtuali vengono implementate (vtables) e la natura del C++ "non si paga finché non lo si usa".

Se non ci sono metodi virtuali in uso, un puntatore all'oggetto punta ai dati dell'oggetto. Non appena viene introdotto un metodo virtuale, il compilatore inserisce una tabella di ricerca virtuale (vtable) e il puntatore punta a questo. Probabilmente mi manca qualcosa (e il mio cervello non funziona ancora) dal momento che non ho potuto ottenere ciò fino a quando non ho inserito un membro dei dati nella classe base. Se la classe base ha un membro dati e la prima classe figlio ha un virtuale, gli offset differiscono per la dimensione del vtable (4 sul mio compilatore).Ecco un esempio che mostra chiaramente:

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; 
} 

L'esecuzione di questo sulla mia macchina (MacBook Pro) stampa il seguente:

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 

La cosa interessante è che sia bObj e cObj mostra il cambiamento di indirizzo tra metodi di chiamata su A e B o C. La differenza è che B contiene un metodo virtuale. Ciò consente al compilatore di inserire la tabella aggiuntiva necessaria per implementare la virtualizzazione delle funzioni. L'altra cosa interessante che questo programma mostra è che typeid(T) e typeid(*ptr) è diverso in B::x quando viene chiamato virtualmente. È inoltre possibile visualizzare un aumento della dimensione utilizzando sizeof non appena viene inserita la tabella virtuale.

Nel tuo caso, non appena hai reso virtuale CWaitable::WakeWaiters, il vtable viene inserito e in effetti presta attenzione al tipo reale dell'oggetto e inserisce le strutture di contabilità necessarie. Ciò fa sì che l'offset alla base dell'oggetto sia diverso. Desidero davvero poter trovare il riferimento che descrive un layout di memoria mitico e perché l'indirizzo di un oggetto dipende dal tipo che viene interpretato come quando l'ereditarietà è mescolata al divertimento.

regola generale: (e avete sentito questo prima) classi di base hanno sempre distruttori virtuali. Ciò contribuirà ad eliminare piccole sorprese come questa.

+0

L'idea è che ogni classe abbia i suoi dati memorizzati in un blocco contiguo di memoria e che i suoi metodi siano generati in modo tale che l'accesso a tali dati sia il più veloce possibile. Pertanto il "questo" puntatore per questi metodi non deve cambiare nello stesso contesto. –

Problemi correlati