【问题标题】:Debugging C++ virtual multiple inheritance in Visual Studio 2008 watch window在 Visual Studio 2008 监视窗口中调试 C++ 虚拟多重继承
【发布时间】:2010-01-08 02:48:15
【问题描述】:

我在 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放在watch窗口中。我想不出办法在监视窗口的 pB 中看到“d”。调试器不接受 C 风格转换或 dynamic_cast。展开到 v-table 表明调试器知道它实际上指向的是 D 析构函数,但无法看到“d”。

从基类定义中删除“虚拟”(所以 D 有 2 个 A),调试器会让我扩展 pB 并看到它确实是一个可以扩展的 D* 对象。这也是我希望在虚拟案例中看到的。

有什么办法可以做到这一点吗?我需要找出对象布局的实际偏移量才能找到它吗?还是说我对虚拟多重继承和重新设计不够聪明,因为实际项目要复杂得多,如果我不能调试,我应该让它更简单:)

【问题讨论】:

  • 你可以试试另一个调试器 :) 也许是windbg?
  • 项目更新:我重构了我的设计以避免虚拟多重继承。我仍在使用多重继承,但不需要虚拟多重继承。当指针指向中间类(示例中为 C*)时,调试器以相同的方式处理多重继承,因此该解决方案仍然有用。

标签: c++ visual-studio-2008 debugging


【解决方案1】:

这个link 还表明调试符号引擎存在与虚拟基类的多重继承问题。

但是如果你只是想帮助调试,为什么不在类 A 上添加一个辅助函数来获取 D 指针(如果可用)。你可以看 pB->GetMyD()。

class D;

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

class D...

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

这会将指针运算留给编译器。

【讨论】:

  • 太好了,我不认为调试器会评估这样的函数调用,但这很好用。仅用于调试构建就足够了。
  • 我终于在实际项目中尝试了这个,但它不起作用:(“会员功能不存在”啊!
  • 啊,实际项目有一个exe/dll,而我要调试的类在dll中。当我调试到 dll 中的函数时,它可以工作。看起来它在 exe/dll 边界上不起作用。足够接近有用。
  • 自原始帖子以来已经有 4 年了,但如果其他人有这个问题:您可以使用语法“{,,name.dll} foo()”从调试器中调用一个函数另一个 DLL。
【解决方案2】:

仔细查看 pB 和 pD 的实际指针值。正确调整指针很难,需要编译器。

【讨论】:

  • Visual Studio 是一个编译器,它没有智能感知、内联解析和监视窗口中的其他表达式的问题 :)
  • Visual Studio 肯定不是编译器,它是一个 IDE。调试器也不是编译器。
  • 好的,语义化。 Visual Studio 与编译器集成得如此紧密,我相信如果该功能存在,它可以解决这个问题。
  • 它知道如何生成 .rsp 文件和解析编译器输出,仅此而已。更换编译器也很简单,英特尔利用了这一点。
【解决方案3】:

在我看来,需要多重继承和虚拟继承的时代非常少见,即便如此,也可能有更好的方法来为域建模。继承本身会在基类和派生类之间创建紧密耦合,因此添加菱形树会创建一堆紧密耦合的类,这些类最终会形成脊状设计。

除此之外。我在vs2003和vs2005中编译了你的代码,它们都在监视窗口中显示了以下内容。

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

【讨论】:

  • 看手表里的pB。它实际上是一个 D 对象,但调试器没有显示,这是我的问题和疑问。你如何让调试器显示它?在代码中创建指向所有可能的派生类的指针以便我可以调试它会很困难。
  • 关于我是否应该使用虚拟多重继承,在实际项目中我试图模拟像 C#/Java 这样的接口。一种纯虚拟类层次结构,表示镜像实现类的公共接口。让它工作的唯一方法是最终基类的虚拟多重继承。这是一个实验,看看它的效果如何。如果我根本无法让它工作,那么到目前为止还不是很好!
【解决方案4】:

好吧,我终于可以使用指针运算来解决问题了,所以我会回答我自己的问题。声明一个全局:

D d;

现在我可以把这个放到调试器中了,我可以看到包含pB指向的B的D对象的内容:

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

所以基本上,我只需要定义一个仅用于调试的 D 实例,我可以使用它来查找指针偏移量。

奇怪的是调试器似乎在做一些运行时类型识别来计算 &d.d 和 &d.b 的地址偏移量。如果我尝试一个不指向 D 实例的内存地址,调试器会给出错误的答案!这个:

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

实际上为两个值显示相同的地址!太奇怪了!

该解决方案并不漂亮,但它确实有效。我可能可以创建仅用于调试的全局变量。调试器似乎应该能够自动获取此信息,但事实并非如此。哦,好吧!

【讨论】:

  • 如果这对您来说是可靠的,请查看virtualdub.org/blog/pivot/entry.php?id=120——您可以使用它来自动执行指针运算。 (有时开始工作有点繁琐,但是一旦开始工作就相当强大并且可以节省大量时间。)
【解决方案5】:

据我所知,实际上没有安全的恢复方式。
如果您查看 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 实际上只在从 D* 转换时才指向 B。
这是由于类的内部表示方式。
正常的继承可以被认为是这样的内存结构:

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 将指向 D 的 base2,而不是具有与普通单继承情况相同的内存起点。

与非虚拟多重继承相同。

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 的内存地址不同,而 pC 与 base1 相同

免责声明!!
上述表述可能并不完全正确。这是一个简化的心智模型,大部分时间都对我有用。在某些情况下,此模型可能不正确。

结论: 多重继承和任何类型的虚拟继承都会阻止 reinterpret_cast 以安全的方式返回子类型。
MS VC++(Visual Studio 中使用的 C++ 编译器)实现非虚拟多重继承的方式,您可以从超类列表中的第一个基类型转换回子类。不知道这是根据 C++ 规范还是其他编译器是怎么做的。

【讨论】:

    【解决方案6】:

    我刚刚将其添加为 C++ 的“最奇怪的语言功能”。你建议编译器坏了,这是可信的。为什么烦恼?不要使用虚拟 MI。

    添加“AProxy”(通过传入的 A ref 构建)并让“具体”类(如 D)包含单个 A 成员,并将其传递给基 B 和 C。

    AProxy 为 A 提供了一个接口,而不是真正的 A——它在构造时委托给 A 绑定。它很丑,但钻石 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_) { }
    }
    

    【讨论】:

      【解决方案7】:

      您可以在 Visual 中做另一件丑陋的事情,这可能会帮助您了解幕后发生的事情。 打开其中一个内存窗口,输入变量名称作为地址,然后打开“自动重新评估”选项。还将列宽设置为 4 个字节,以便成员很好地对齐。

      对另一个变量执行相同的操作,并与监视窗口一起查看对象的内容,并显示子类型如何堆叠在一起并组成派生类型。

      您最终应该得到一些指向各种 vf 表和整数成员的指针。 vf 表指针很有趣,因为它们告诉您对象的实际类型。 但是,您将需要在每个派生类中重新声明至少一个虚拟方法,以便每个类都获得一个新的 vf 表。重新声明析构函数应该可以解决问题。

      希望这能对其中发生的事情有所了解。 干杯。

      【讨论】:

        猜你喜欢
        • 2017-03-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-08-13
        • 2012-10-11
        • 2010-10-24
        • 2014-11-06
        • 1970-01-01
        相关资源
        最近更新 更多