2009-03-21 3 views
6

Я столкнулся с тем, что кажется действительно раздражающей ошибкой, запускающей мою программу на C++ под Microsoft Visual C++ 2003, но это может быть просто то, что я делаю неправильно, поэтому подумал Я бы бросил его здесь и посмотрел, есть ли у кого-нибудь идеи.C++ «this» не соответствует методу объекта, вызванному на

У меня есть иерархия классов, как это (точно так, как есть - например, нет множественного наследования в реальном коде):

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

Хорошо, так что я называю «AnnoyingFunctionThatBreaks» с экземпляром «CMotionWalk» (например, отладчик говорит, что это 0x06716fe0), и все выглядит хорошо. Но когда я вхожу в него, к точке останова при вызове «DoStuff», указатель «this» имеет другое значение для указателя pMotion, на который я вызвал метод (например, теперь отладчик говорит одно слово выше - 0x06716fe4).

К слову это по-другому: pMotion имеет значение 0x06716fe0, но когда я вызываю метод на нем, этот метод видит 'this' как 0x06716fe4.

Я не собираюсь сходить с ума, не так ли? Это странно, правда?

+0

Нарезка происходит на объектах, а не на указателях. Код, который вы опубликовали, работает после небольшого исправления. Отправьте некоторый реальный код - мой кристаллический шар не работает сегодня. Кстати: вы действительно не собираетесь проходить «это», не так ли? – dirkgently

+0

Это почти наверняка многократное наследование - образец кода, вероятно, был обрезан. –

+0

@Earwicker: Откуда вы получили множественное наследство? – dirkgently

ответ

10

Я считаю, что вы просто видите артефакт того, как компилятор создает vtables. Я подозреваю, что у CMotion есть свои виртуальные функции, и поэтому вы получаете смещения внутри производного объекта, чтобы добраться до базового объекта. Таким образом, разные указатели.

Если он работает (т. Е. Если это не приводит к сбоям, и нет указателей вне объектов), то я бы не стал слишком беспокоиться об этом.

+0

Ну, это не работает, потому что метод DoStuff путается, если «это» не то же самое, что и раньше. Но вы точно знаете свое объяснение - я добавлю это к вопросу, что у CMotion есть виртуальные функции, поэтому объявление CWaitable :: WakeWaiters как виртуального решения проблемы. – andygeers

+0

Я не понимаю, как смещение может измениться. производный класс имеет базовый класс, и оба они начинаются в одном и том же месте в памяти. почему на самом деле смещение должно отличаться без множественного наследования? делает msvc что-то странное? –

+0

Я понятия не имею, как MSVC устанавливает вещи в памяти, но если вы думаете об этом, то должно быть где-то внутри объекта Derived, где начинается базовый объект. В зависимости от прихоти компилятора можно поместить производные элементы до или после базовых элементов. –

2

См. Также wikipedia article on thunking. Если вы установите отладчик для прохождения кода сборки, вы должны увидеть, как это происходит. (будь то удар или просто изменение смещения зависит от деталей, которые вы указали из кода, который вы указали)

+0

Я попытался пройти через код сборки. Я действительно не знаю, что я ищу, но это не выглядело так необычно. – andygeers

0

Вам необходимо ввести фактический код. Значения указателей в следующие соединения, как ожидается, - то есть они одинаковы:

#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

ли CMOTION класс получения какой-либо другой класс, который содержит также виртуальную функцию? Я обнаружил, что этот указатель не изменяется с кодом вас в курсе, однако она меняется, если у вас есть иерархия что-то вроде этого:

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

Я считаю, что это из-за множественного наследования для класса CMOTION и виртуальные таблицы указателя в CMotion, который указывает на Test :: f()

+0

Там определенно не существует множественного наследования. – andygeers

0

Я не могу объяснить, почему это работает, но декларирование CWaitable :: WakeWaiters как виртуальные устраняет проблему

1

Я думаю, что я могу объяснить этот ... есть лучшее объяснение где-то в одном из либо Мейера или книги Саттера, но я не хотел искать. Я считаю, что то, что вы видите, является следствием реализации виртуальных функций (vtables) и «вы не платите за нее, пока не используете ее», характер C++.

Если виртуальных методов не используется, указатель на объект указывает на данные объекта. Как только виртуальный метод вводится, компилятор вставляет таблицу виртуального поиска (vtable), и вместо этого указатель указывает на это. Я, вероятно, что-то пропустил (и мой мозг пока не работает), так как я не мог это сделать, пока не вставил элемент данных в базовый класс. Если базовый класс имеет элемент данных, а первый дочерний класс имеет виртуальный, то смещения отличаются размером vtable (4 в моем компиляторе).Вот пример, который показывает, что это ясно:

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

Запуск этого на моей машине (MacBook Pro) печатает следующее:

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 

Интересно то, что оба bObj и cObj демонстрируют изменение адреса, как между вызывающие методы на A и B или C. Разница в том, что B содержит виртуальный метод. Это позволяет компилятору вставить дополнительную таблицу, необходимую для реализации виртуализации функций. Другая интересная вещь, которую показывает эта программа, заключается в том, что typeid(T) и typeid(*ptr) отличается от B::x, когда ее называют практически. Вы также можете увидеть увеличение размера с помощью sizeof, как только вставлена ​​виртуальная таблица.

В вашем случае, как только вы сделали CWaitable::WakeWaiters виртуальным, вставлена ​​таблица vtable, и на самом деле она обращает внимание на реальный тип объекта, а также на вставку необходимых бухгалтерии. Это приводит к тому, что смещение базы объекта будет отличаться. Я действительно хочу, чтобы я мог найти ссылку, описывающую мифический макет памяти, и почему адрес объекта зависит от типа, который он интерпретируется, когда наследование смешивается с потерей.

Общее правило: (и вы слышали это раньше) базовые классы всегда имеют виртуальные деструкторы. Это поможет устранить небольшие сюрпризы, подобные этому.

+0

Идея состоит в том, что каждый класс имеет свои данные, хранящиеся в непрерывном блоке памяти, и его методы генерируются таким образом, чтобы доступ к этим данным был как можно быстрее. Поэтому «этот» указатель для этих методов не должен меняться в одном контексте. –

Смежные вопросы