【问题标题】:When is a vtable created in C++?何时在 C++ 中创建 vtable?
【发布时间】:2010-12-30 03:40:30
【问题描述】:

编译器究竟什么时候创建虚函数表?

1) 当类包含至少一个虚函数时。

2) 当直接基类包含至少一个虚函数时。

3) 当层次结构中任何级别的任何父类至少包含一个虚函数时。

与此相关的问题: 是否可以放弃 C++ 层次结构中的动态调度?

例如考虑下面的例子。

#include <iostream>
using namespace std;
class A {
public:
  virtual void f();
};
class B: public A {
public:
  void f();
};
class C: public B {
public:
  void f();
};

哪些类将包含 V-Table?

既然 B 没有将 f() 声明为虚拟,那么类 C 是否获得动态多态性?

【问题讨论】:

  • 除非你正在编写一个编译器(你不是因为你问了这个问题),否则它与任何 C++ 用户完全无关。该语言甚至没有 vTable 的概念。
  • 除非你有一些关于现代微处理器设计的下一个架构突破的内部信息,否则我会停止所有关于 vtables 不是语言一部分的宣传。在 C++ 文献中到处都提到了 Vtables(不是标准)。其次,现代项目经常选择 C++,因为它生成的确定性代码——知道生成了一个 vtable 是这方面的重要组成部分。
  • @LokiAstari - 了解 C++ 不仅仅是肤浅的语言。另外,如果你对事物的工作原理不感兴趣,你就不是真正的程序员;]
  • @Di-0xide:在 C++ 中,除了担心不会影响您编写代码的深奥实现细节之外,还有更多的东西需要理解。即使你认为你知道它们是如何工作的,你仍然真的不知道。唯一真正了解 v-table 复杂性的人是已经实现它们的人(而且周围几乎没有 C++ 编译器编写者)其他人(根据我的经验)猜测通常是错误的。
  • @Di-0xide:即使您手动操作了 v-tables(您不是,不要傻到声称您是)。您无法从这样的讨论中获得任何信息。没有足够的空间来提供所需的详细信息。

标签: c++ polymorphism virtual-functions vtable


【解决方案1】:

答案是“视情况而定”。这取决于您所说的“包含 vtbl”是什么意思,并且取决于特定编译器的实现者做出的决定。

严格来说,没有任何“类”包含虚函数表。某些类的某些实例包含指向虚函数表的指针。然而,这只是语义的一种可能实现。

在极端情况下,编译器可以假设将唯一编号放入实例中,该编号索引到用于选择适当虚函数实例的数据结构中。

如果您问“GCC 是做什么的?”或“Visual C++ 做什么?”那么你就可以得到一个具体的答案了。

@Hassan Syed 的回答可能更接近您的问题,但在这里保持概念清晰非常重要。

行为(基于新的类的动态调度)和实现。您的问题使用了实施术语,但我怀疑您正在寻找行为答案。

行为答案是这样的:任何声明或继承虚函数的类都会在调用该函数时表现出动态行为。任何没有的课程,都不会。

在实现方面,允许编译器做任何它想做的事情来完成那个结果。

【讨论】:

  • 哦,笨蛋,我把你变成了香蕉。 (“我知道怎么拼香蕉,只是不知道什么时候该停下来。”)已修复。
【解决方案2】:

回答

当类声明包含虚函数时,会创建一个 vtable。当父级(层次结构中的任何位置)具有虚函数时,就会引入 vtable,让我们将此父级称为 Y。Y 的任何父级都不会有 vtable(除非他们的层次结构中有 virtual 用于其他功能)。

继续阅读以进行讨论和测试

-- 解释--

当您将成员函数指定为虚拟时,您可能会尝试在运行时通过基类多态地使用子类。为了保持 c++ 对语言设计性能的保证,他们提供了最轻量级的实现策略——即,一个间接级别,并且仅当一个类可能在运行时多态地使用时,程序员通过设置至少一个函数来指定这一点虚拟的。

如果您避免使用 virtual 关键字,则不会产生 vtable 的成本。

-- 编辑:反映您的编辑--

只有当基类包含虚函数时,其他子类才会包含 vtable。所述基类的父母没有vtable。

在您的示例中,所有三个类都有一个 vtable,这是因为您可以尝试通过 A*. 使用所有三个类

--test - GCC 4+ --

#include <iostream>

class test_base
{
  public:
    void x(){std::cout << "test_base" << "\n"; };
};

class test_sub : public test_base
{
public:
  virtual void x(){std::cout << "test_sub" << "\n"; } ;
};

class test_subby : public test_sub
{
public:
  void x() { std::cout << "test_subby" << "\n"; }
};

int main() 
{
  test_sub sub;
  test_base base;
  test_subby subby;

  test_sub * psub;
  test_base *pbase;
  test_subby * psubby;

  pbase = &sub;
  pbase->x();
  psub = &subby;
  psub->x();

  return 0;
}

输出

test_base
test_subby

test_base 没有虚拟表,因此任何投射到它的东西都将使用来自test_basex()。另一方面,test_sub 改变了 x() 的性质,它的指针将通过 vtable 间接传递,这可以通过正在执行的 test_subbyx() 来显示。

因此,只有在使用关键字 virtual 时才会在层次结构中引入 vtable。较早的祖先没有 vtable,如果发生向下转换,它将被硬连线到祖先函数。

【讨论】:

  • 我想你的意思是当一个类包含一个虚函数时
【解决方案3】:

C++ 标准不要求使用 V-Tables 来创建多态类的错觉。大多数情况下,实现使用 V-Tables 来存储所需的额外信息。简而言之,当你至少有一个虚拟功能时,这些额外的信息就会被配备。

【讨论】:

    【解决方案4】:

    除了“vtables are implementation-specific”(它们是)之外,如果使用 vtable:每个类都会有唯一的 vtables。即使 B::fC::f 没有声明为虚拟,因为在来自基类 (A 在您的代码中),B::fC::f 都是隐式虚拟的。因为每个类至少有一个唯一的虚方法(B::f 覆盖 A::f 对于 B 实例和 C: :f 对于 C 实例类似),您需要三个 vtable。

    您通常不必担心这些细节。重要的是你是否有虚拟调度。你不必使用虚拟调度,通过显式指定要调用的函数,但这通常只在实现虚拟方法时有用(例如调用基地的方法)。示例:

    struct B {
      virtual void f() {}
      virtual void g() {}
    };
    
    struct D : B {
      virtual void f() { // would be implicitly virtual even if not declared virtual
        B::f();
        // do D-specific stuff
      }
      virtual void g() {}
    };
    
    int main() {
      {
        B b; b.g(); b.B::g(); // both call B::g
      }
      {
        D d;
        B& b = d;
        b.g(); // calls D::g
        b.B::g(); // calls B::g
    
        b.D::g(); // not allowed
        d.D::g(); // calls D::g
    
        void (B::*p)() = &B::g;
        (b.*p)(); // calls D::g
        // calls through a function pointer always use virtual dispatch
        // (if the pointed-to function is virtual)
      }
      return 0;
    }
    

    一些可能有所帮助的具体规则;但不要在这些方面引用我的话,我可能错过了一些极端情况:

    • 如果一个类有虚方法或虚基,即使继承,实例也必须有一个虚表指针。
    • 如果一个类声明了非继承的虚方法(例如当它没有基类时),那么它必须有自己的 vtable。
    • 如果一个类的覆盖方法集与其第一个基类不同,则它必须有自己的 vtable,并且不能重用基类。 (析构函数通常需要这个。)
    • 如果一个类有多个基类,而第二个或以后的基类有虚方法:
      • 如果早期的基类没有虚方法,并且空基优化已应用于所有早期的基类,则将此基类视为第一个基类。
      • 否则,该类必须有自己的 vtable。
    • 如果一个类有任何虚拟基类,它必须有自己的 vtable。

    请记住,vtable 类似于类的静态数据成员,实例只有指向它们的指针。

    另请参阅 Jan Gray 撰写的综合文章 C++: Under the Hood(1994 年 3 月)。(如果链接失效,则为 Try Google。)

    重用vtable的例子:

    struct B {
      virtual void f();
    };
    struct D : B {
      // does not override B::f
      // does not have other virtuals of its own
      void g(); // still might have its own non-virtuals
      int n; // and data members
    };
    

    特别注意,B 的 dtor 不是虚拟的(这可能是真实代码中的错误),但在本例中,D 个实例将指向与 B 个实例相同的 vtable。

    【讨论】:

      【解决方案5】:

      您努力使您的问题非常清晰和准确,但仍然缺少一些信息。你可能知道,在使用 V-Table 的实现中,表本身通常是一个独立的数据结构,存储在多态对象之外,而对象本身只存储一个隐式的 pointer 到表。那么,你在问什么?可能是:

      • 对象何时会获得一个隐式指针,指向插入的 V-Table?

      • 什么时候为层次结构中的给定类型创建专用的、单独的 V-Table?

      第一个问题的答案是:当对象是多态类类型时,对象会获得一个隐式指针,指向插入其中的 V-Table。如果类类型至少包含一个虚函数,或者它的任何直接或间接父类是多态的,则该类类型是多态的(这是您集合中的答案 3)。另请注意,在多重继承的情况下,一个对象可能(并且将)最终包含嵌入其中的多个 V-Table 指针。

      第二个问题的答案可能与第一个问题的答案相同(选项 3),但可能存在例外。如果单继承层次结构中的某些多态类没有自己的虚函数(没有新的虚函数,没有父虚函数的覆盖),则实现可能决定不为此类创建单独的 V-Table,而是也将它的直接父级 V-Table 用于此类(因为无论如何它都将是相同的)。 IE。在这种情况下,父类型的对象和派生类型的对象都将在它们嵌入的 V-Table 指针中存储相同的值。当然,这高度依赖于实施。我检查了 GCC 和 MS VS 2005,但他们并没有那样做。在这种情况下,他们都为派生类创建了一个单独的 V-Table,但我似乎记得听说过没有的实现。

      【讨论】:

      • +1 用于区分实际的 vtable 和指向 vtable 的指针。
      【解决方案6】:

      行为在 C++ 语言规范的第 10.3 章第 2 段中定义:

      如果一个虚成员函数 vf 是 在类 Base 和 类派生,直接派生或 间接来自成员 Base 具有相同名称的函数 vf 和 与 Base::vf 相同的参数列表是 声明,然后 Derived::vf 也是 虚拟(不管是不是这样 声明 ),它会覆盖 Base::vf。

      相关短语用斜体表示。因此,如果你的编译器创建了通常意义上的 v-tables,那么所有的类都会有一个 v-table,因为它们的所有 f() 方法都是虚拟的。

      【讨论】:

        猜你喜欢
        • 2011-04-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-07-19
        • 2011-12-02
        • 2011-08-03
        相关资源
        最近更新 更多