开发者

Debugging C++ virtual multiple inheritance in Visual Studio 2008 watch window

I'm having trouble debugging a project in Visual Studio C++ 2008 with pointers to objects that have virtual multiple inheritance. I'm unable to examine the fields in the derived class, if the pointer is a type of a base.

A simple test case I made:

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

Put a breakpoint on the "return(0)", and put pD and pB in the watch window. I can't figure out a way to see "d" in开发者_运维技巧 the pB in the watch window. The debugger won't accept a C style cast, or dynamic_cast. Expanding to the v-table shows that the debugger knows it's actually pointing a D destructor, but no way to see "d".

Remove the "virtual's" from the base class definitions (so D has 2 A's) and the debugger will let me expand pB and see that it's really a D* object which can be expanded. This is what I want to see in the virtual case as well.

Is there any way to make this work? Do i need to figure out the actual offsets of the object layout to find it? Or is it time to just say I'm not smart enough for virtual multiple inheritance and redesign, cause the actual project is much more complicated, and if I can't debug, I should make it simpler :)


This link also indicates that the debug symbol engine has problems with multiple inheritance with virtual base classes.

But if you just want help debugging, why not add a helper function on the class A to get a D pointer if available. You can watch pB->GetMyD().

class D;

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

class D...

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

That will leave the pointer arithmetic to the compiler.


Take a close look at the actual pointer value for pB and pD. Getting that pointer adjustment correct is hard, it takes a compiler.


In my opinion the times when multiple & virtual inheritance are needed very few and far between, and even then there are probably better ways to model the domain. Inheritance in itself creates a tight coupling between the base & derived classes, so adding in a diamond tree creates a bunch of tightly coupled classes that will end up in a ridged design.

Aside from that. I compiled your code in vs2003 & vs2005, both of them showed the following in the watch window.

pD               
 + B   { b=6 }
 + C   { c=9 }
   d   12


Well, i got something to work finally playing with pointer arithmetic, so I'll answer my own question. Declaring a global:

D d;

Now I can put this in the debugger, I can see the contents of the D object containing the B that pB points to:

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

So basically, I just need to define a debug only D instance that I can use to find the pointer offsets.

What's odd is the debugger seems to be doing something with run time type identification to figure out the address offsets of &d.d and &d.b. If I try a memory address that isn't pointing to a D instance, the debugger gives the wrong answer! This:

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

actually show the same address for both values! Totally weird!

The solution isn't pretty but it works. I can probably create debug only global variables to use. It seems like the debugger should be able to get this info automatically, but it doesn't. Oh well!


Actually there is no safe way of recovering as far as I know.
If you look at the memory addresses for pB and pD you notice that they are not the same.

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

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

Since you no longer have the original start address, you can't recover. Even a reinterpret_cast will silently fail. It will give you a D* but with wrong values since it will start at 0x00999730 instead of 0x00999720. (reinterpret_cast does not work in the watch window)

This will result in the same thing:

(D*)(void*)pB

Works in the watch window but will show the wrong values since the memory pointed out actually starts at 0x00999730 instead of original 0x00999720.

In your example, reinterpret_cast will result in:

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

obviously wrong, should have been:

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

So it is the original dynamic_cast that messes with things.

EDIT (Additional stuff to note):
What messes things up is that you assumed that pB actually still was a D, which it is not. Due to virtual inheritance, pB actually only points to a B when it gets casted from D*.
This due to how classes are internally represented.
Normal inheritance can be thought of as resulting in a memory structure like this:

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

while virtual inheritance results in something like this:

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

This since virtual inheritance is meant to prevent duplication, which it does using pointers. If you have:

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

D can be thought of something like this:

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

where B's and C's A* base points to the same instance of A. Hence when you cast D to B, instead of having the same memory starting point as in the case of normal single inheritance, pB will point to D's base2.

Samething with none-virtual multiple inheritance.

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

will result in a memory structure that can be thought of as:

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

So if you do this:

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

it to will fail, since pB actually points to base2 which is not at the same memory address as pC which is the same as base1

DISCLAIMER!!
The above representation may not be entirely correct. It is a simplified mental model which has shown to work for me most of the time. There might be scenarios where this model isn't correct.

Conclusion: Multiple inheritance and any kind of virtual inheritance prevent reinterpret_cast back to the subtype in a safe manner.
The way MS VC++ (C++ compiler used in Visual Studio) implements none-virtual multi-inheritance you can cast back from the first base type in the list of super-classes back to subclass. Don't know if this is according to C++ specification or how other compilers do.


I just added this as my "strangest language feature" for C++. You suggest the compiler is broken, and that is believable. Why fret? Don't use virtual MI.

Add "AProxy" (constructed with passed in A ref) and have "concrete" classes like D contain a single A member, passing it down to the bases B and C.

AProxy provides an interface to an A without really being an A -- it delegates to the A bound at construction. It's ugly, but so is diamond MI.

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_) { }
}


There is another ugly thing you can do in Visual which might help you with seeing what goes on under the hood. Turn on one of the memory windows, put in the name of your variable as the address and turn on the "Reevaluate automatically" option. Also set the column width to 4 bytes so the members align nicely.

Do the same for the other variable and together with the watch windows you can look up the contents of your the objects and shows how the sub-types stack up together and compose the derived types.

You should end up with some pointers to the various vf tables and your integer members. The vf table pointers are interesting because they tell you the actual types of the objects. However you WILL need to re-declare at least one virtual method in each derived class so that each class gets a new vf table. Re-declaring the destructor should do the trick.

Hope this sheds some light on what goes on in there. Cheers.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜