虚拟类的每个对象都有一个指向 vtable 的指针吗?

发布于 2024-07-14 00:25:16 字数 102 浏览 8 评论 0原文

虚拟类的每个对象都有一个指向 vtable 的指针吗?

还是只有带有虚函数的基类对象才有?

vtable存储在哪里? 进程的代码部分还是数据部分?

Does every object of virtual class have a pointer to vtable?

Or only the object of base class with virtual function has it?

Where did the vtable stored? code section or data section of process?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

瑾夏年华 2024-07-21 00:25:16

所有具有虚拟方法的类都会有一个由该类的所有对象共享的 vtable。

每个对象实例都有一个指向该 vtable 的指针(这就是 vtable 的查找方式),通常称为 vptr。 编译器隐式生成代码来初始化构造函数中的 vptr。

请注意,这些都不是 C++ 语言强制要求的 - 如果需要,实现可以通过其他方式处理虚拟调度。 然而,这是我熟悉的每个编译器都使用的实现。 Stan Lippman 的书“Inside the C++ Object Model”描述了它是如何很好地工作的。

All classes with a virtual method will have a single vtable that is shared by all objects of the class.

Each object instance will have a pointer to that vtable (that's how the vtable is found), typically called a vptr. The compiler implicitly generates code to initialize the vptr in the constructor.

Note that none of this is mandated by the C++ language - an implementation can handle virtual dispatch some other way if it wants. However, this is the implementation that is used by every compiler I'm familiar with. Stan Lippman's book, "Inside the C++ Object Model" describes how this works very nicely.

夜未央樱花落 2024-07-21 00:25:16

正如其他人所说,C++ 标准并不强制要求使用虚拟方法表,但允许使用虚拟方法表。 我已经使用 gcc 和此代码以及最简单的可能场景之一完成了测试:

class Base {
public: 
    virtual void bark() { }
    int dont_do_ebo;
};

class Derived1 : public Base {
public:
    virtual void bark() { }
    int dont_do_ebo;
};

class Derived2 : public Base {
public:
    virtual void smile() { }
    int dont_do_ebo;
};

void use(Base* );

int main() {
    Base * b = new Derived1;
    use(b);

    Base * b1 = new Derived2;
    use(b1);
}

添加数据成员以防止编译器将基类的大小设置为零(称为空基类)优化)。 这是 GCC 选择的布局:(使用 -fdump-class-hierarchy 打印)

Vtable for Base
Base::_ZTV4Base: 3u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI4Base)
8     Base::bark

Class Base
   size=8 align=4
   base size=8 base align=4
Base (0xb7b578e8) 0
    vptr=((& Base::_ZTV4Base) + 8u)

Vtable for Derived1
Derived1::_ZTV8Derived1: 3u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI8Derived1)
8     Derived1::bark

Class Derived1
   size=12 align=4
   base size=12 base align=4
Derived1 (0xb7ad6400) 0
    vptr=((& Derived1::_ZTV8Derived1) + 8u)
  Base (0xb7b57ac8) 0
      primary-for Derived1 (0xb7ad6400)

Vtable for Derived2
Derived2::_ZTV8Derived2: 4u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI8Derived2)
8     Base::bark
12    Derived2::smile

Class Derived2
   size=12 align=4
   base size=12 base align=4
Derived2 (0xb7ad64c0) 0
    vptr=((& Derived2::_ZTV8Derived2) + 8u)
  Base (0xb7b57c30) 0
      primary-for Derived2 (0xb7ad64c0)

如您所见,每个类都有一个 vtable。 前两个条目很特别。 第二个指向该类的RTTI数据。 第一个 - 我知道但忘记了。 它在更复杂的情况下有一些用处。 好吧,正如布局所示,如果你有一个 Derived1 类的对象,那么 vptr(v-table-pointer)当然会指向 Derived1 类的 v-table,它的函数 bark 正好有一个条目指向Derived1 的版本。 Derived2 的 vptr 指向 Derived2 的 vtable,该表有两个条目。 另外一个就是它新增的一个方法,微笑。 它重复了 Base::bark 的条目,这当然会指向该函数的 Base 版本,因为它是它的最派生版本。

我还使用 -fdump-tree-optimized 转储了一些优化完成后由 GCC 生成的树(构造函数内联,...)。 输出使用 GCC 的中端语言 GIMPL ,它是独立于前端的,缩进到一些类似 C 的块结构中:

;; Function virtual void Base::bark() (_ZN4Base4barkEv)
virtual void Base::bark() (this)
{
<bb 2>:
  return;
}

;; Function virtual void Derived1::bark() (_ZN8Derived14barkEv)
virtual void Derived1::bark() (this)
{
<bb 2>:
  return;
}

;; Function virtual void Derived2::smile() (_ZN8Derived25smileEv)
virtual void Derived2::smile() (this)
{
<bb 2>:
  return;
}

;; Function int main() (main)
int main() ()
{
  void * D.1757;
  struct Derived2 * D.1734;
  void * D.1756;
  struct Derived1 * D.1693;

<bb 2>:
  D.1756 = operator new (12);
  D.1693 = (struct Derived1 *) D.1756;
  D.1693->D.1671._vptr.Base = &_ZTV8Derived1[2];
  use (&D.1693->D.1671);
  D.1757 = operator new (12);
  D.1734 = (struct Derived2 *) D.1757;
  D.1734->D.1682._vptr.Base = &_ZTV8Derived2[2];
  use (&D.1734->D.1682);
  return 0;    
}

正如我们可以很好地看到的,它只是设置一个指针 - vptr - 这将指向我们之前在创建对象时见过的适当的虚函数表。 我还转储了用于创建 Derived1 的汇编代码并调用使用($4 是第一个参数寄存器,$2 是返回值寄存器,$0 是始终为 0 的寄存器)。 c++filt 工具:)

      # 1st arg: 12byte
    add     $4, $0, 12
      # allocate 12byte
    jal     operator new(unsigned long)    
      # get ptr to first function in the vtable of Derived1
    add     $3, $0, vtable for Derived1+8  
      # store that pointer at offset 0x0 of the object (vptr)
    stw     $3, $2, 0
      # 1st arg is the address of the object
    add     $4, $0, $2
    jal     use(Base*)

如果我们想调用 bark 会发生什么?:

void doit(Base* b) {
    b->bark();
}

GIMPL 代码:

;; Function void doit(Base*) (_Z4doitP4Base)
void doit(Base*) (b)
{
<bb 2>:
  OBJ_TYPE_REF(*b->_vptr.Base;b->0) (b) [tail call];
  return;
}

OBJ_TYPE_REF 是一个 GIMPL 构造,它被漂亮地打印到(它是记录在gcc SVN源代码中的gcc/tree.def中)

OBJ_TYPE_REF(<first arg>; <second arg> -> <third arg>)

它的含义是:在对象b上使用表达式*b->_vptr.Base,并存储前端 (c++) 特定值 0 (它是 vtable 的索引)。 最后,它传递 b 作为“this”参数。 我们是否会调用出现在 vtable 中第二个索引处的函数(注意,我们不知道哪个 vtable 属于哪种类型!),GIMPL 看起来像这样:

OBJ_TYPE_REF(*(b->_vptr.Base + 4);b->1) (b) [tail call];

当然,这里又是汇编代码(堆栈帧的东西) cut off):

  # load vptr into register $2 
  # (remember $4 is the address of the object, 
  #  doit's first arg)
ldw     $2, $4, 0
  # load whatever is stored there into register $2
ldw     $2, $2, 0
  # jump to that address. note that "this" is passed by $4
jalr    $2

记住 vptr 正好指向第一个函数。 (在该条目之前存储了 RTTI 时隙)。 因此,无论出现在该插槽中的内容都将被调用。 它还将调用标记为尾调用,因为它是我们的 doit 函数中的最后一个语句。

Like someone else said, the C++ Standard does not mandate a virtual method table, but allows one to be used. I've done my tests using gcc and this code and one of the simplest possible scenario:

class Base {
public: 
    virtual void bark() { }
    int dont_do_ebo;
};

class Derived1 : public Base {
public:
    virtual void bark() { }
    int dont_do_ebo;
};

class Derived2 : public Base {
public:
    virtual void smile() { }
    int dont_do_ebo;
};

void use(Base* );

int main() {
    Base * b = new Derived1;
    use(b);

    Base * b1 = new Derived2;
    use(b1);
}

Added data-members to prevent the compiler to give the base-class a size-of of zero (it's known as the empty-base-class-optimization). This is the layout that GCC chose: (print using -fdump-class-hierarchy)

Vtable for Base
Base::_ZTV4Base: 3u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI4Base)
8     Base::bark

Class Base
   size=8 align=4
   base size=8 base align=4
Base (0xb7b578e8) 0
    vptr=((& Base::_ZTV4Base) + 8u)

Vtable for Derived1
Derived1::_ZTV8Derived1: 3u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI8Derived1)
8     Derived1::bark

Class Derived1
   size=12 align=4
   base size=12 base align=4
Derived1 (0xb7ad6400) 0
    vptr=((& Derived1::_ZTV8Derived1) + 8u)
  Base (0xb7b57ac8) 0
      primary-for Derived1 (0xb7ad6400)

Vtable for Derived2
Derived2::_ZTV8Derived2: 4u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI8Derived2)
8     Base::bark
12    Derived2::smile

Class Derived2
   size=12 align=4
   base size=12 base align=4
Derived2 (0xb7ad64c0) 0
    vptr=((& Derived2::_ZTV8Derived2) + 8u)
  Base (0xb7b57c30) 0
      primary-for Derived2 (0xb7ad64c0)

As you see each class has a vtable. The first two entries are special. The second one points to the RTTI data of the class. The first one - i knew it but forgot. It's got some use in more complicated cases. Well, as the layout shows, if you have an object of class Derived1, then the vptr (v-table-pointer) will point to the v-table of class Derived1 of course, which has exactly one entry for its function bark pointing to Derived1's version. Derived2's vptr points to Derived2's vtable, which has two entries. The other one is the new method that's added by it, smile. It repeats the entry for Base::bark, which will point to Base's version of the function of course, because it's the most derived version of it.

I've also dumped the tree that's generated by GCC after some optimizations are done (constructor inlined, ...), with -fdump-tree-optimized. The output is using GCC's middle-end language GIMPL which is front-end independent, indented into some C-like block structure:

;; Function virtual void Base::bark() (_ZN4Base4barkEv)
virtual void Base::bark() (this)
{
<bb 2>:
  return;
}

;; Function virtual void Derived1::bark() (_ZN8Derived14barkEv)
virtual void Derived1::bark() (this)
{
<bb 2>:
  return;
}

;; Function virtual void Derived2::smile() (_ZN8Derived25smileEv)
virtual void Derived2::smile() (this)
{
<bb 2>:
  return;
}

;; Function int main() (main)
int main() ()
{
  void * D.1757;
  struct Derived2 * D.1734;
  void * D.1756;
  struct Derived1 * D.1693;

<bb 2>:
  D.1756 = operator new (12);
  D.1693 = (struct Derived1 *) D.1756;
  D.1693->D.1671._vptr.Base = &_ZTV8Derived1[2];
  use (&D.1693->D.1671);
  D.1757 = operator new (12);
  D.1734 = (struct Derived2 *) D.1757;
  D.1734->D.1682._vptr.Base = &_ZTV8Derived2[2];
  use (&D.1734->D.1682);
  return 0;    
}

As we can see nicely, it's just setting one pointer - the vptr - which will point to the appropriate vtable we have seen before when creating the object. I've also dumped the assembler code for the creation of the Derived1 and call to use ($4 is first argument register, $2 is return value register, $0 is always-0-register) after demangling the names in it by the c++filt tool :)

      # 1st arg: 12byte
    add     $4, $0, 12
      # allocate 12byte
    jal     operator new(unsigned long)    
      # get ptr to first function in the vtable of Derived1
    add     $3, $0, vtable for Derived1+8  
      # store that pointer at offset 0x0 of the object (vptr)
    stw     $3, $2, 0
      # 1st arg is the address of the object
    add     $4, $0, $2
    jal     use(Base*)

What happens if we want to call bark?:

void doit(Base* b) {
    b->bark();
}

GIMPL code:

;; Function void doit(Base*) (_Z4doitP4Base)
void doit(Base*) (b)
{
<bb 2>:
  OBJ_TYPE_REF(*b->_vptr.Base;b->0) (b) [tail call];
  return;
}

OBJ_TYPE_REF is a GIMPL construct which is pretty printed into (it's documented in gcc/tree.def in the gcc SVN source-code)

OBJ_TYPE_REF(<first arg>; <second arg> -> <third arg>)

It's meaning: Use the expression *b->_vptr.Base on the object b, and store the frontend (c++) specific value 0 (it's the index into the vtable). Finally, it's passing b as the "this" argument. Would we call a function that appears at the 2nd index in the vtable (note, we don't know which vtable of which type!), the GIMPL would look like this:

OBJ_TYPE_REF(*(b->_vptr.Base + 4);b->1) (b) [tail call];

Of course, here the assembly code again (stack-frame stuff cut off):

  # load vptr into register $2 
  # (remember $4 is the address of the object, 
  #  doit's first arg)
ldw     $2, $4, 0
  # load whatever is stored there into register $2
ldw     $2, $2, 0
  # jump to that address. note that "this" is passed by $4
jalr    $2

Remember that the vptr points exactly at the first function. (Before that entry the RTTI slot were stored). So, whatever appears at that slot is called. It's also marking the call as tail-call, because it happens as the last statement in our doit function.

流心雨 2024-07-21 00:25:16

Vtable 是每个类的实例,即,如果我有 10 个具有虚拟方法的类的对象,则只有一个 vtable 在所有 10 个对象之间共享。

本例中所有 10 个对象都指向同一个 vtable。

Vtable is a per class instance, i.e., if I have 10 objects of a class which has a virtual method there is only one vtable which is shared among all the 10 objects.

All the 10 objects in this case point to same vtable.

-柠檬树下少年和吉他 2024-07-21 00:25:16

在家试试这个:

#include <iostream>
struct non_virtual {}; 
struct has_virtual { virtual void nop() {} }; 
struct has_virtual_d : public has_virtual { virtual void nop() {} }; 

int main(int argc, char* argv[])
{
   std::cout << sizeof non_virtual << "\n" 
             << sizeof has_virtual << "\n" 
             << sizeof has_virtual_d << "\n";
}

Try this at home:

#include <iostream>
struct non_virtual {}; 
struct has_virtual { virtual void nop() {} }; 
struct has_virtual_d : public has_virtual { virtual void nop() {} }; 

int main(int argc, char* argv[])
{
   std::cout << sizeof non_virtual << "\n" 
             << sizeof has_virtual << "\n" 
             << sizeof has_virtual_d << "\n";
}
蘑菇王子 2024-07-21 00:25:16

VTable 是一个实现细节,语言定义中没有任何内容表明它存在。 事实上,我已经阅读过有关实现虚拟函数的替代方法。

但是:所有常见的编译器(即我所知道的编译器)都使用 VTabel。
好的。 任何具有虚拟方法的类或从具有虚拟方法的类(直接或间接)派生​​的类都将具有带有指向 VTable 的指针的对象。

您提出的所有其他问题将取决于编译器/硬件,这些问题没有真正的答案。

A VTable is an implementation detail there is nothing in the language definition that says it exists. In fact I have read about alternative methods for implementing virtual functions.

BUT: All the common compilers (ie the ones I know about) use VTabels.
Then Yes. Any class that has a virtual method or is derived from a class (directly or indirectly) that has a virtual method will have objects with a pointer to a VTable.

All other questions you ask will depend on the compiler/hardware there is no real answer to those questions.

是伱的 2024-07-21 00:25:16

要回答哪些对象(从现在开始的实例)具有 vtable 以及在哪里的问题,考虑一下何时需要 vtable 指针会很有帮助。

对于任何继承层次结构,您都需要为该层次结构中特定类定义的每组虚拟函数提供一个 vtable。 换句话说,考虑到以下情况:

class A { virtual void f(); int a; };
class B: public A { virtual void f(); virtual void g(); int b; };
class C: public B { virtual void f(); virtual void g(); virtual void h(); int c; };
class D: public A { virtual void f(); int d; };
class E: public B { virtual void f(); int e; };

因此,您需要五个 vtable:A、B、C、D 和 E 都需要自己的 vtable。

接下来,您需要知道在给定特定类的指针或引用的情况下使用哪个 vtable。 例如,给定一个指向 A 的指针,您需要足够了解 A 的布局,以便获得一个 vtable,告诉您将 A::f() 分派到何处。 给定一个指向 B 的指针,您需要足够了解 B 的布局才能调度 B::f() 和 B::g()。 等等等等。

一种可能的实现可以将 vtable 指针作为任何类的第一个成员。 这意味着 A 实例的布局将是:

A's vtable;
int a;

B 实例的布局将是:

A's vtable;
int a;
B's vtable;
int b;

并且您可以从此布局生成正确的虚拟调度代码。

您还可以通过组合具有相同布局的 vtable 指针或者其中一个 vtable 是另一个 vtable 的子集来优化布局。 因此,在上面的示例中,您还可以将 B 布局为:

B's vtable;
int a;
int b;

因为 B 的 vtable 是 A 的超集。 B 的 vtable 具有 A::f 和 B::g 条目,A 的 vtable 具有 A::f 条目。

为了完整起见,这就是您迄今为止所看到的所有 vtable 的布局方式:

A's vtable: A::f
B's vtable: A::f, B::g
C's vtable: A::f, B::g, C::h
D's vtable: A::f
E's vtable: A::f, B::g

实际的条目将是:

A's vtable: A::f
B's vtable: B::f, B::g
C's vtable: C::f, C::g, C::h
D's vtable: D::f
E's vtable: E::f, B::g

对于多重继承,您进行相同的分析:

class A { virtual void f(); int a; };
class B { virtual void g(); int b; };
class C: public A, public B { virtual void f(); virtual void g(); int c; };

最终的布局将是:

A: 
A's vtable;
int a;

B:
B's vtable;
int b;

C:
C's A vtable;
int a;
C's B vtable;
int b;
int c;

您需要一个指向 vtable 的指针与 A 兼容并且指向与 B 兼容的 vtable 的指针,因为对 C 的引用可以转换为 A 或 B 的引用,并且需要将虚函数分派给 C。

由此可以看出,特定的 vtable 指针的数量class has 至少是它派生自的根类的数量(直接派生或由于超类派生)。 根类是具有 vtable 的类,该类不是从也具有 vtable 的类继承的。

虚拟继承带来了另一点间接性,但您可以使用相同的度量来确定 vtable 指针的数量。

To answer the question about which objects (instances from now on) have vtables and where, it's helpful to think about when you need a vtable pointer.

For any inheritance hierarchy, you need a vtable for each set of virtual functions defined by a particular class in that hierarchy. In other words, given the following:

class A { virtual void f(); int a; };
class B: public A { virtual void f(); virtual void g(); int b; };
class C: public B { virtual void f(); virtual void g(); virtual void h(); int c; };
class D: public A { virtual void f(); int d; };
class E: public B { virtual void f(); int e; };

As a result, you need five vtables: A, B, C, D, and E all need their own vtables.

Next, you need to know what vtable to use given a pointer or reference to a particular class. E.g., given a pointer to A, you need to know enough about the layout of A such that you can get a vtable that tells you where to dispatch A::f(). Given a pointer to B, you need to know enough about the layout of B to dispatch B::f() and B::g(). And so on and so on.

One possible implementation could put a vtable pointer as the first member of any class. That would mean the layout of an instance of A would be:

A's vtable;
int a;

And an instance of B would be:

A's vtable;
int a;
B's vtable;
int b;

And you could generate correct virtual dispatching code from this layout.

You can also optimize the layout by combining vtable pointers of vtables that have the same layout or if one is a subset of the other. So in the above example, you could also layout B as:

B's vtable;
int a;
int b;

Because B's vtable is a superset of A's. B's vtable has entries for A::f and B::g, and A's vtable has entries for A::f.

For completeness, this is how you would layout all the vtables we've seen so far:

A's vtable: A::f
B's vtable: A::f, B::g
C's vtable: A::f, B::g, C::h
D's vtable: A::f
E's vtable: A::f, B::g

And the actual entries would be:

A's vtable: A::f
B's vtable: B::f, B::g
C's vtable: C::f, C::g, C::h
D's vtable: D::f
E's vtable: E::f, B::g

For multiple inheritance, you do the same analysis:

class A { virtual void f(); int a; };
class B { virtual void g(); int b; };
class C: public A, public B { virtual void f(); virtual void g(); int c; };

And the resultant layouts would be:

A: 
A's vtable;
int a;

B:
B's vtable;
int b;

C:
C's A vtable;
int a;
C's B vtable;
int b;
int c;

You need a pointer to a vtable compatible with A and a pointer to a vtable compatible with B because a reference to C can be converted to a reference of A or B and you need to dispatch virtual functions to C.

From this you can see that the number of vtable pointers a particular class has is at least the number of root classes it derives from (either directly or due to a superclass). A root class is a class that has a vtable that does not inherit from a class that also has a vtable.

Virtual inheritance throws another bit of indirection into the mix, but you can use the same metric to determine the number of vtable pointers.

微暖i 2024-07-21 00:25:16

所有虚拟类通常都有一个 vtable,但 C++ 标准并不要求它,并且存储方法取决于编译器。

All virtual classes usually have a vtable, but it is not required by the C++ standard and the storage method is dependent upon the compiler.

沒落の蓅哖 2024-07-21 00:25:16

每个多态类型的对象都会有一个指向Vtable的指针。

VTable 存储的位置取决于编译器。

Every object of polymorphic type will have a pointer to Vtable.

Where VTable stored is dependant on compiler.

筑梦 2024-07-21 00:25:16

不一定

几乎每个具有虚函数的对象都会有一个虚函数表指针。 对于具有派生对象的虚函数的每个类,不需要有一个 v 表指针。

不过,在某些情况下,充分分析代码的新编译器可能能够消除 v 表。

例如,在一种简单的情况下:如果只有一个抽象基类的一个具体实现,编译器知道它可以将虚拟调用更改为常规函数调用,因为每当调用虚拟函数时,它总是会解析为确切的函数相同的功能。

此外,如果只有几个不同的具体函数,编译器可能会有效地更改调用站点,以便它使用“if”来选择要调用的正确具体函数。

因此,在这种情况下,不需要 v 表,并且对象最终可能没有 v 表。

Not necessarily

Pretty much every object that has a virtual function will have one v-table pointer. There doesn't need to be a v-table pointer for each class that has a virtual function that the object derives from.

New compilers that analyse the code sufficiently may be able to eliminate v-tables in some cases though.

For example, in a simple case: if you only have one concrete implementation of an abstract base class, the compiler knows that it can change the virtual calls to be regular function calls because whenever the virtual function is called it will always resolve to the exact same function.

Also, if there's only a couple of different concrete functions, the compiler may effectively change the call-site so that it uses an 'if' to select the right concrete function to call.

So, in cases like this the v-table isn't needed and the objects might end up not have one.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文