C ++ «this» не соответствует объектному методу - PullRequest
6 голосов
/ 21 марта 2009

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

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

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.

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

Ответы [ 6 ]

10 голосов
/ 21 марта 2009

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

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

6 голосов
/ 21 марта 2009

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

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 и указателя vtable в CMotion, который указывает на Test :: f ()

2 голосов
/ 21 марта 2009

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

1 голос
/ 22 марта 2009

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

Если виртуальные методы не используются, указатель на объект указывает на данные объекта. Как только виртуальный метод введен, компилятор вставляет виртуальную таблицу поиска (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 голосов
/ 21 марта 2009

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

0 голосов
/ 21 марта 2009

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

#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 );
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...