2009-03-21 10 views
6

Natknąłem się na coś, co wydaje się bardzo irytującym błędem z moim programem C++ pod Microsoft Visual C++ 2003, ale może to być po prostu coś, co robię źle, tak myślałem Wyrzuciłbym to tutaj i sprawdziłbym, czy ktoś ma jakieś pomysły.C++ "to" nie pasuje do metody obiektu zostało wywołane na

Mam hierarchię klas, takich jak ten (dokładnie jak jest - na przykład nie ma wielokrotne dziedziczenie w rzeczywistym kodzie):

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

Ok, więc ja nazywam „AnnoyingFunctionThatBreaks” z „CMotionWalk” instancji (np. debugger mówi, że to 0x06716fe0) i wszystko wydaje się dobrze. Ale kiedy wkraczam w to, do punktu przerwania połączenia z "DoStuff", "ten" wskaźnik ma inną wartość niż wskaźnik pMotion, który nazwałem metodą (np. Teraz debugger mówi jedno słowo wyżej - 0x06716fe4).

Aby sformułować to inaczej: pMotion ma wartość 0x06716fe0, ale kiedy wywołuję metodę na niej, ta metoda widzi "to" jako 0x06716fe4.

Nie szaleję, prawda? To dziwne, prawda?

+0

Krojenie odbywa się na obiektach, a nie na wskaźnikach. Kod, który wysłałeś, działa po drobnym poprawieniu. Opublikuj jakiś prawdziwy kod - moja krystaliczna piłka nie działa dzisiaj. BTW: tak naprawdę nie zamierzasz tego "omawiać", prawda? – dirkgently

+0

To prawie na pewno wielokrotne dziedziczenie - prawdopodobnie próbka kodu została nadmiernie przycięta. –

+0

@Earwicker: Skąd otrzymałeś wiele dziedziczenia? – dirkgently

Odpowiedz

10

Wierzę, że po prostu widzisz artefakt sposobu, w jaki kompilator buduje vtables. Podejrzewam, że CMotion ma swoje własne wirtualne funkcje, a więc kończy się przesunięciem w obiekcie pochodnym, aby dostać się do obiektu bazowego. Tak więc różne wskaźniki.

Jeśli działa (tzn. Jeśli nie powoduje awarii, a poza obiektami nie ma wskaźników), nie martwiłbym się tym zbytnio.

+0

Cóż, to nie działa, ponieważ metoda DoStuff jest zdezorientowana, jeśli "to" nie jest tym samym, co wcześniej. Ale jesteś dokładnie na miejscu z twoim wyjaśnieniem - dodam to do pytania, że ​​CMotion ma funkcje wirtualne, więc deklarowanie CWaitable :: WakeWaiters jako wirtualnego rozwiązuje problem. – andygeers

+0

Nie rozumiem, jak może się zmienić przesunięcie. klasa pochodna ma klasę podstawową i obie zaczynają się w tym samym miejscu w pamięci. dlaczego na ziemi powinno być inne przesunięcie bez wielokrotnego dziedziczenia? czy msvc robi coś dziwnego? –

+0

Nie mam pojęcia, jak MSVC ustawia rzeczy w pamięci, ale jeśli o tym pomyśleć, musi być gdzieś w obiekcie Derived, w którym zaczyna się obiekt Base. Możliwe jest umieszczanie wyprowadzonych elementów PRZED lub PO NIM elementami bazowymi, w zależności od kaprysu kompilatora. –

2

Zobacz także wikipedia article on thunking. Jeśli ustawisz debugger, aby przejrzał kod assemblera, powinieneś to zobaczyć. (Bez względu na to, czy jest to thunk, czy po prostu zmiana offsetu zależy od szczegółów, które usunąłeś z podanego kodu)

+0

Próbowałem przejść przez kod zespołu. Naprawdę nie wiem, czego szukam, ale nie wyglądało to tak nietypowo – andygeers

0

Musisz zamieścić prawdziwy kod. Wartości dla wskaźników w następujących są zgodnie z oczekiwaniami - to znaczy, że są takie same:

#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

klasa CMotion jest pochodzący jakąś inną klasę, która zawiera również funkcję wirtualnego? Okazało się, że ten wskaźnik nie zmienia się kod, który pisał, zmienia to jednak jeśli masz hierarchii coś takiego:

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

Wierzę, że to ze względu na wielokrotne dziedziczenie w klasie CMotion i vtable wskaźnik w CMotion, która wskazuje na Test :: f()

+0

Z pewnością nie ma żadnego wielokrotnego dziedziczenia. – andygeers

0

nie mogę wyjaśnić, dlaczego to działa, ale deklarujące CWaitable :: WakeWaiters jako wirtualny rozwiązuje problem

1

myślę, że mogę wyjaśnić tę jedną ... tam jest lepsze wyjaśnienie gdzieś w jednej albo Meyer lub książki Suttera, ale nie miałem ochoty szukać. Wierzę, że to, co widzisz, jest konsekwencją implementacji funkcji wirtualnych (vtables) i "nie płacisz za to, dopóki nie użyjesz" natury C++.

Jeśli nie są używane żadne metody wirtualne, wskaźnik do obiektu wskazuje dane obiektu. Natychmiast po wprowadzeniu metody wirtualnej kompilator wstawia wirtualną tabelę odnośników (vtable), a wskaźnik wskazuje na to. Prawdopodobnie brakuje mi czegoś (a mój mózg jeszcze nie działa), ponieważ nie mogłem tego zrobić, dopóki nie wstawiłem elementu danych w klasie bazowej. Jeśli klasa podstawowa ma element danych, a pierwsza klasa potomna ma wartość wirtualną, wówczas przesunięcia różnią się rozmiarem tabeli vtable (4 w moim kompilatorze).Oto przykład, który pokazuje to jasno:

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

Running to na moim komputerze (MacBook Pro) drukuje następujące:

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 

Interesującą rzeczą jest to, że zarówno bObj i cObj eksponat zmiana adresu między metody wywoływania na A i B lub C. Różnica polega na tym, że B zawiera metodę wirtualną. Dzięki temu kompilator może wstawić dodatkową tabelę niezbędną do wdrożenia wirtualizacji funkcji. Inną interesującą rzeczą, którą pokazuje ten program, jest to, że typeid(T) i typeid(*ptr) jest inny w B::x, gdy jest nazywany wirtualnie. Możesz również zobaczyć wzrost rozmiaru przy użyciu sizeof natychmiast po wstawieniu wirtualnej tabeli.

W twoim przypadku, gdy tylko utworzysz CWaitable::WakeWaiters wirtualny, zostanie wstawiony vtable, który faktycznie zwraca uwagę na rzeczywisty typ obiektu, jak również wstawienie niezbędnych struktur księgowych. Powoduje to, że przesunięcie do podstawy obiektu się różni. Naprawdę chciałbym móc znaleźć odniesienie, które opisuje mityczny układ pamięci i dlaczego adres obiektu zależy od typu, który jest interpretowany, gdy dziedziczenie jest mieszane w zabawę.

Zasada ogólna: (a słyszeliście to już wcześniej) klasy bazowe zawsze mają destruktory wirtualne. Pomoże to wyeliminować takie niespodzianki.

+0

Chodzi o to, że każda klasa ma swoje dane przechowywane w ciągłym bloku pamięci, a jej metody są generowane w taki sposób, że dostęp do tych danych jest tak szybki, jak to możliwe. Dlatego "ten" wskaźnik dla tych metod nie musi się zmieniać w tym samym kontekście. –

Powiązane problemy