27

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

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 类会获得动态多态性吗?

4

6 回答 6

24

除了“vtables 是特定于实现的”(它们是)之外,如果使用 vtable:每个类都会有唯一的 vtables。即使B::fC::f没有声明为虚拟的,因为在基类(代码中的A )的虚拟方法上有匹配的签名, B::fC::f都是隐式虚拟的. 因为每个类至少有一个唯一的虚方法(B::f覆盖B实例的 A::f 和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 月)。(如果该链接失效,请尝试谷歌。)

重用 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 。

于 2009-12-26T18:22:52.353 回答
8

答案是“视情况而定”。这取决于“包含 vtbl”的含义,并且取决于特定编译器的实现者做出的决定。

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

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

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

@Hassan Syed 的答案可能更接近您的要求,但在这里保持概念直截了当非常重要。

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

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

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

于 2009-12-26T18:08:45.580 回答
7

回答

当类声明包含虚函数时,会创建一个 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没有虚拟表,因此任何投射到它的东西都将使用x()from test_basetest_sub另一方面,改变了的性质,x()它的指针将间接通过一个vtable,这通过test_subby'x()被执行来显示。

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

于 2009-12-26T18:11:29.083 回答
3

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

  • 一个对象什么时候得到一个指向插入到它的 V-Table 的隐式指针?

或者

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

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

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

于 2009-12-26T18:58:29.270 回答
2

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

于 2009-12-26T18:15:11.950 回答
1

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

如果虚成员函数 vf 在类 Base 和 Derived 类中声明,直接或间接从 Base 派生,则声明与 Base::vf 具有相同名称和相同参数列表的成员函数 vf,则 Derived::vf也是虚拟的(无论它是否如此声明)并且它覆盖 Base::vf。

A 斜体表示相关短语。因此,如果您的编译器在通常意义上创建 v-table,那么所有类都将具有 v-table,因为它们的所有 f() 方法都是虚拟的。

于 2009-12-26T19:01:30.467 回答