Отладка виртуального множественного наследования C ++ в окне просмотра Visual Studio 2008 - PullRequest
4 голосов
/ 08 января 2010

У меня проблемы с отладкой проекта в Visual Studio C ++ 2008 с указателями на объекты, имеющие виртуальное множественное наследование. Я не могу проверить поля в производном классе, если указатель является типом базы.

Простой тестовый пример, который я сделал:

class A
{
    public:
        A() { a = 3; };
        virtual ~A() {}
        int a;
};

class B : virtual public A
{
    public:
        B() { b = 6; }
        int b;
};

class C : virtual public A
{
    public:
        C() { c = 9; }
        int c;      
};

class D : virtual public B, virtual public C
{
    public:
        D() { d = 12; }
        int d;
};

int main(int argc, char **argv)
{
    D *pD = new D();
    B *pB = dynamic_cast<B*>(pD);

    return(0);
}

Установите точку останова на "return (0)" и поместите pD и pB в окно просмотра. Я не могу понять, как увидеть «d» в пб в окне просмотра. Отладчик не будет принимать приведение в стиле C или dynamic_cast. Развертывание до v-таблицы показывает, что отладчик знает, что он на самом деле указывает на деструктор D, но не видит «d».

Удалите "виртуальные" из определений базового класса (таким образом, у D есть 2 A), и отладчик позволит мне расширить pB и увидеть, что это действительно объект D *, который может быть расширен. Это то, что я хочу видеть и в виртуальном случае.

Есть ли способ заставить эту работу? Мне нужно выяснить фактические смещения макета объекта, чтобы найти его? Или пришло время просто сказать, что я недостаточно умен для виртуального множественного наследования и редизайна, потому что сам проект намного сложнее, и если я не могу отладить, я должен сделать его проще:)

Ответы [ 7 ]

1 голос
/ 09 января 2010

Эта ссылка также указывает, что у механизма символов отладки есть проблемы с множественным наследованием с виртуальными базовыми классами.

Но если вам нужна помощь в отладке, почему бы не добавить вспомогательную функцию в класс A, чтобы получить указатель D, если он доступен. Вы можете посмотреть pB-> GetMyD ().

class D;

class A 
{
    ...
    D* GetMyD();
    ...
}

class D...

D* A::GetMyD()
{
   return dynamic_cast<D*>(this);
}

Это оставит арифметику указателя для компилятора.

1 голос
/ 08 января 2010

Ну, у меня наконец-то получилось поиграть с арифметикой указателей, поэтому я отвечу на свой вопрос.Объявление глобального:

D d;

Теперь я могу поместить это в отладчик, я могу видеть содержимое объекта D, содержащего B, на который указывает pB:

(D*)((char *) pB + (((char *)&d.d) - ((char *)&d.b)))

Так что в основномМне просто нужно определить экземпляр D только для отладки, который я могу использовать, чтобы найти смещения указателя.

Странно, что отладчик, кажется, что-то делает с определением типа во время выполнения, чтобы выяснить смещения адреса & d.d и & d.b.Если я пробую адрес памяти, который не указывает на экземпляр D, отладчик дает неправильный ответ!Это:

&((D *)(void *) pB)->b
&((D *)(void *) pB)->d

фактически показывает один и тот же адрес для обоих значений!Совершенно странно!

Решение не красивое, но оно работает.Я могу, вероятно, создать отладку только глобальные переменные для использования.Кажется, что отладчик должен иметь возможность автоматически получать эту информацию, но это не так.О, хорошо!

1 голос
/ 08 января 2010

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

Помимо этого. Я скомпилировал ваш код в vs2003 и vs2005, оба они показали следующее в окне просмотра.

pD               
 + B   { b=6 }
 + C   { c=9 }
   d   12
1 голос
/ 08 января 2010

Присмотритесь к фактическому значению указателя для pB и pD. Получить правильную настройку указателя сложно, для этого нужен компилятор.

0 голосов
/ 23 февраля 2010

В Visual вы можете сделать еще одну уродливую вещь, которая может помочь вам увидеть, что происходит под капотом. Включите одно из окон памяти, введите имя переменной в качестве адреса и включите опцию «Переоценивать автоматически». Также установите ширину столбца в 4 байта, чтобы элементы хорошо совмещались.

Сделайте то же самое для другой переменной, и вместе с окнами наблюдения вы можете просмотреть содержимое ваших объектов и показать, как подтипы складываются вместе и составляют производные типы.

Вы должны получить несколько указателей на различные таблицы vf и ваших целочисленных членов. Указатели таблицы vf интересны тем, что они сообщают вам фактические типы объектов. Однако вам нужно будет повторно объявить по крайней мере один виртуальный метод в каждом производном классе, чтобы каждый класс получил новую таблицу vf. Повторное объявление деструктора должно сработать.

Надеюсь, это проливает некоторый свет на то, что там происходит. Приветствия.

0 голосов
/ 10 января 2010

Я только что добавил это как "странную языковую функцию" для C ++. Вы предполагаете, что компилятор сломан, и это правдоподобно. Зачем раздражаться? Не используйте виртуальный МИ.

Добавьте «AProxy» (созданный с использованием переданного в A ref), и «конкретные» классы, такие как D, содержат один член A, передавая его основам B и C.

AProxy предоставляет интерфейс к A, фактически не являясь A - он делегирует A, связанный при построении. Это уродливо, но так же, как и алмазный МИ.

struct AProxy {  
  const A& a_;  
  AProxy(const A& a) : a_(a) { }  
}
struct B : public AProxy ... 
   B(const A& a) : AProxy(a) { } 
struct C : public AProxy ... 
struct D : public B, public C { 
  A a_;
  D() : a_(), B(a_) C(a_) { }
}
0 голосов
/ 09 января 2010

На самом деле, насколько я знаю, безопасного способа восстановления нет.
Если вы посмотрите на адреса памяти для pB и pD, вы заметите, что они не совпадают.

D *pD = new D(); // points at 0x00999720

B *pB = dynamic_cast<B*>(pD); // points at 0x00999730, 
// hence inside the memory segment of pD

Так как у вас больше нет первоначального начального адреса, вы не можете восстановить. Даже reinterpret_cast молча провалится. Это даст вам D *, но с неправильными значениями, так как он будет начинаться с 0x00999730 вместо 0x00999720. (reinterpret_cast не работает в окне просмотра)

Это приведет к тому же:

(D*)(void*)pB

Работает в окне просмотра, но покажет неправильные значения, поскольку указанная память фактически начинается с 0x00999730 вместо исходного 0x00999720.

В вашем примере reinterpret_cast приведет к:

D* pD2 = reinterpret_cast<D*>(pB); // or "(D*)(void*)pD" in the watch window
pD2
 + B {b=6}
   + A {a=3}
 + C {c=6}
   + A {a=3}    
 d=6

очевидно, неправильно, должно было быть:

 + B {b=6}
   + A {a=3}
 + C {c=9}
   + A {a=3}    
 d=12

Так что это оригинальный dynamic_cast, который портится с вещами.

РЕДАКТИРОВАТЬ (дополнительный материал для заметки):
Что запутывает, так это то, что вы предположили, что pB на самом деле все еще был D, а это не так. Из-за виртуального наследования pB фактически указывает на B только тогда, когда он кастуется из D *.
Это связано с тем, как классы представлены внутри.
Нормальное наследование может рассматриваться как приводящее к структуре памяти, подобной этой:

struct A
{
    int a;
}
struct B
{
    A base
    int b;
}

в то время как виртуальное наследование приводит к чему-то вроде этого:

struct A
{
    int a;
}
struct B
{
    A* base
    int b;
}

Это связано с тем, что виртуальное наследование предназначено для предотвращения дублирования, которое осуществляется с помощью указателей. Если у вас есть:

class A
class B: virtual public A
class C: virtual public A
class D: virtual public B, virtual public C

D можно представить примерно так:

struct D
{
    B* base1;
    C* base2;
    int d;
}

, где B и C * A указывают на один и тот же экземпляр A. Следовательно, когда вы приводите D к B, вместо того, чтобы иметь ту же начальную точку памяти, как в случае обычного одиночного наследования, pB будет указывать на base2 D.

То же самое с не-виртуальным множественным наследованием.

class A
class B
class C: public A, public B

приведет к структуре памяти, которая может быть представлена ​​как:

struct C
{
    A base1;
    B base2;
    int c;
}

Итак, если вы сделаете это:

{
    C *pC = new C();
    B *pB = dynamic_cast<B*>(pC);
    C *pC2 = reinterpret_cast<C*>(pB);
}

это не удастся, так как pB фактически указывает на base2, который не находится на том же адресе памяти, что и pC, который совпадает с base1

ОТКАЗ !!
Приведенное выше представление может быть не совсем правильным. Это упрощенная ментальная модель, которая показала, что работает для меня большую часть времени. Могут быть сценарии, в которых эта модель неверна.

Вывод: Множественное наследование и любой вид виртуального наследования предотвращают безопасное возвращение reinterpret_cast к подтипу.
Способ, которым MS VC ++ (компилятор C ++, используемый в Visual Studio) реализует не-виртуальное мульти-наследование, которое можно преобразовать из первого базового типа в списке суперклассов обратно в подкласс. Не знаю, соответствует ли это спецификации C ++ или другим компиляторам.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...