【问题标题】:Why are my vfptr table entries mixed up?为什么我的 vfptr 表条目混淆了?
【发布时间】:2014-09-30 04:02:28
【问题描述】:

我对以下问题感到困惑(MSVC++ 2012):

我有一组定义接口的纯虚拟类和一些用于实现的具有多重继承的派生类。我使用同一组基类来实现两个不同的端类(在随后的类层次结构中,“Foo”类为这两个端类中的每一个实现不同的虚函数)。这两个最终类之一编译没有问题,但另一个将其 vfptr 表条目混合在一起。我对可能导致此问题的原因感到困惑,并且无法指出错误。所以我把它提交给这里的 C++ 专家。

这是简化的类层次结构:

class IBaz1 {
   virtual void IBaz1_SC() = 0;
   virtual void IBaz1_IVR() = 0;
   virtual void IBaz1_ICIVR() = 0;
};

class IBaz2 {
   virtual void IBaz2_JTHL() = 0;
   virtual void IBaz2_UP() = 0;
   virtual void IBaz2_RKF() = 0;
   virtual void IBaz2_GVC() = 0;
   virtual void IBaz2_GAI() = 0;
};

class IBaz3 {
   virtual void IBaz3_SCJ() = 0;
   virtual void IBaz3_CJS() = 0;
};

class IQux {
   virtual void IQux_RIU() = 0;
   virtual void IQux_CTAU() = 0;
   virtual void IQux_D() = 0;
   virtual void IQux_AD() = 0;
   virtual void IQux_IDIP() = 0;
   virtual void IQux_GLT() = 0;
   virtual void IQux_SLT() = 0;
   virtual void IQux_GF() = 0;
};

class Qux : public IQux {
   virtual void Qux::IQux_D();
   virtual void Qux::IQux_AD();
   virtual void Qux::IQux_IDIP();
};

class Bar : public IBaz1, public IBaz2, public IBaz3, public Qux {
   virtual void IBaz1_ICIVR();
   virtual void IBaz2_UP();
   virtual void IQux_RIU();
   virtual void IQux_GLT();
   virtual void IQux_SLT();
   virtual void IQux_GF();
};

class FooA (and FooB) : public Bar {
   virtual void IBaz1_SC();
   virtual void IBaz1_IVR();
   virtual void IBaz2_JTHL();
   virtual void IBaz2_RKF();
   virtual void IBaz2_GVC();
   virtual void IBaz2_GAI();
   virtual void IBaz3_SCJ();
   virtual void IBaz3_CJS();
   virtual void IQux_CTAU();
};

      Expected vfptr             result mixed-up vfptr
      ======================     ====================================
FooA (is also a COM)       FooB (is a straight inheritance)
  Bar                        Bar
    IBaz1                      IBaz1
      [0] Foo::IBaz1_SC()        [0] Foo::IBaz1_SC()
      [1] Foo::IBaz1_IVR()       [1] Foo::IBaz1_IVR()
      [2] Bar::IBaz1_ICIVR()     [2] Bar::IBaz1_ICIVR()
    IBaz2                      IBaz2
      [0] Foo::IBaz2_JTHL()      [0] Bar::IQux_GLT()
      [1] Bar::IBaz2_UP()        [1] Bar::IQux_SLT()
      [2] Foo::IBaz2_RKF()       [2] Foo::IBaz2_JTHL()
      [3] Foo::IBaz2_GVC()       [3] Bar::IBaz2_UP()
      [4] Foo::IBaz2_GAI()       [4] Foo::IBaz2_RKF()
    IBaz3                      IBaz3
      [0] Foo::IBaz3_SCJ()       [0] Foo::IBaz3_SCJ()
      [1] Foo::IBaz3_CJS()       [1] Foo::IBaz3_CJS()
    Qux                        Qux
      IQux                       IQux
        [0] Bar::IQux_RIU()        [0] Bar::IQux_RIU()
        [1] Foo::IQux_CTAU()       [1] Foo::IQux_CTAU()
        [2] Qux::IQux_D()          [2] Qux::IQux_D()
        [3] Qux::IQux_AD()         [3] Qux::IQux_AD()
        [4] Qux::IQux_IDIP()       [4] Qux::IQux_IDIP()
        [5] Bar::IQux_GLT()        [5] [thunk]:Bar::IQux_GLT`adjustor{8}'()
        [6] Bar::IQux_SLT()        [6] [thunk]:Bar::IQux_SLT`adjustor{8}'()
        [7] Bar::IQux_GF()         [7] [thunk]:Bar::IQux_GF`adjustor{8}'()

两个端类是“FooA”和“FooB”。在实际代码中,它们的定义方式相同,只是 FooA 也是一个 AxtiveX COM 对象,因此除了继承自 Bar,因此继承自 IBaz1、IBaz2、IBaz3 和 Qux 之外,它还继承自许多 ATL 模板。

我已将名称缩减为大写,以便它们适合页面宽度。比如IBaz1_SC的真名在实际代码中是“SetCapture”,IBaz1_IVR真名是“InvalidateViewportRect”。

我并排显示了两个 vfptr 表,以便于比较它们。我展示的 vfptr 表布局是我在为所有继承类展开 vtable 后从 MSVC 调试器“自动”窗口获得的布局。左边的 vfptr 表来自构建和运行良好的最终类,即“FooA”,它也是一个 COM 继承类。而正确的 vfptr 表来自未运行的最终类,即“FooB”,它不是 COM 继承的类,而只是一个直接继承的类。

注意 IBaz2 vfptr 中的条目。 IBaz2[0] 中的指针已下推至 IBaz2[2],IBaz2[1] 下推至 IBaz2[3] 等。IBaz2[0] 和 [1] 中的条目指向来自 IQuz[5] 的函数和 [6]。现在 IQux[5] 到 [7] 中的指针正在经历一个 thunk 并且有一个调整器。

程序在某些时候崩溃,它调用 IBaz2_JTHL 而不是调用 IBaz2_RKF。

这可能是什么原因造成的?我应该寻找什么样的编码错误?任何可能有助于理解问题和解决问题的解释将不胜感激。

如果需要,我可以提供更好的代码摘录。

【问题讨论】:

  • 某事告诉我您的问题在于您尚未发布的最终课程代码,如果它适用于其中一个而不适用于另一个,您为什么不尝试发布不起作用的课程?在不相关的说明中,您的所有类都没有 virtual 析构函数,除非您只使用堆栈上的对象,否则这很危险。
  • 对不起,对我来说代码几乎无法阅读。如果真的有问题,是否可以将示例最小化到读者能够发现问题的程度?源代码中不可读的名称是破坏代码的第一步。
  • “其 vfptr 表条目混淆了”。那是你对事实的解释。让我们先看看事实本身。你不是在展示它们。如果您的程序有意外行为,请描述预期行为和实际行为,并显示完整的源代码以便重现。
  • 为什么没有虚拟析构函数?
  • 我已经编辑了 OP,试图让两个 enc 类更加明确,解释示例中使用的名称(这不仅仅是一个玩具示例,这是来自一个拥有数十万的真实应用程序代码行,仅涉及的实际类将长达几页),并使其更明确地预期结果与获得的结果。

标签: c++ virtual vtable


【解决方案1】:

从提出的问题来看,很明显生成的 vtable 布局与您的期望不符。这很可能是由于您的期望而不是其他原因。

调整器重击通常是由非零偏移的基类引起的,而基类又是由多重继承引起的。

众所周知,MSVC 会折叠相同的函数,这可能会导致混淆。例如,如果其中一些函数是无操作的,它们的 vtable 条目可能都指向相同的无操作实现。

您的代码无法运行可能是因为您绕过了 C++ 并直接访问了 vtable(我假设这是因为您显示了一个 vtable 布局,它甚至没有在 C++ 标准中定义)

[编辑]

新信息几乎证实了这一点:非标准访问 = COM。 COM 不如 C++ 强大,不能处理多重继承。 COM 会误解来自使用 MI 的 C++ 类的 vtable,从而导致上述问题。

【讨论】:

  • 感谢“无操作”指示。我会检查的。我不直接访问 vtable。我并排显示的布局是我在调试器中看到的布局,我将指针展开到 vtables。
  • 好吧,COM 会为您访问它们。最后还是同样的问题。
  • 运行流畅的是COM继承的FooA。直接的非 COM FooB 是其 vtable 混淆的那个。我正在编辑 OP 以使其更加明确。此外,没有一个纯虚函数暴露在 COM 接口中。它们仅在内部调用。
【解决方案2】:

所以我找到了这个 vtable 混淆的原因。 IBaz2 类被定义了两次。第二个定义在一个旧的源文件中,该文件不久前已从项目中删除,但仍在磁盘上并在预编译头文件中引用。

第二个和旧的定义包含所有显示在错误 vtable 中的纯虚函数。我假设,因为它在预编译头文件中,旧定义在编译时用于 vtable 布局,但访问它的代码是从新定义生成的。

为了弄清楚这一点,我开始重命名所涉及的类。在修复重命名相关问题时,我偶然发现了对旧文件的引用。

现在一切都按预期进行。我本来希望编译器会抱怨类的第二个定义。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-05-21
    • 2010-09-07
    • 1970-01-01
    • 1970-01-01
    • 2019-03-09
    • 2011-03-01
    • 2021-04-06
    • 1970-01-01
    相关资源
    最近更新 更多