接口vtable

15

接口(仅具有纯虚函数的多态类)是否具有虚表? 由于接口本身不实现多态函数且无法直接构造,因此链接器无需放置虚表。这样是吗?我特别关注MSVC编译器。


一个非常重要的注意事项是MSVC中的declspec(novtable)特性:它允许接口,特别是COM接口,省略vtable。当涉及到继承时,这具有一些有趣和重要的影响(强制单一继承,但导致最终对象只有一个表提供比其他情况下更多的多态性)。 - ssube
3个回答

9
是的,它们确实有。这其中有很多好的原因。
首先,即使是纯虚函数也有实现。无论是隐式还是显式的实现。调用一个纯虚函数可以相对容易地进行技巧处理,因此你基本上可以为自己的函数提供定义,然后调用它并查看结果。因此,首先应该有一个虚表。
即使所有方法都是纯虚的,而且没有其他数据成员,将虚表放入基类中的另一个原因是当使用多态性时,指向基类的指针在整个程序中传递。为了调用虚方法,编译器/运行时应该确定虚表与基指针之间的相对偏移量。如果C++没有多重继承,可以假设从抽象基类(例如)开始的偏移量为零,在这种情况下,不需要在那里使用vtable(但由于原因#1,我们仍然需要它)。但由于涉及到多重继承,所以“vtable在0偏移处”这样的技巧是行不通的,因为可能会有两个或三个vtable,这取决于基类的数量(和类型)。
还可能有其他我没有想到的原因。
希望能对您有所帮助。

好的论点!我没有想到那个。 - Luchian Grigore
2
你所提到的技巧,是指那种既不需要定义纯虚函数(根据_odr_规则),也不会因为我不知道任何这样的技巧而导致未定义行为吗? - CB Bailey
3
我对你的第一个理由并不信服。我相当确定只有两种方法可以调用纯虚函数:(a)非虚拟地调用它,这不使用虚表,或者(b)通过在部分构造对象的构造函数/析构函数中调用它来调用未定义行为,这根本不需要调用它。你知道其他我不知道的技巧吗? - Mike Seymour
@MikeSeymour:我猜这取决于未定义的定义有多好。我不是一个标准迷。在实践中,它可能取决于实现,但非常明确定义。另一个技巧是 - 找出vtable中方法的偏移量并直接调用它。但vtable必须存在,可以是函数/方法的地址0x0,也可以是用户提供的定义或默认实现的地址。 - user405725

6

从纯C++的角度来看,这是一个学术性问题。虚函数不一定要用vtable来实现,如果使用了vtable,就没有通用的方法可以访问它们。

如果你特别关心MSVC编译器,你可能想要在接口中使用 __declspec(novtable) 进行修饰。

(通常情况下,一个抽象类可能需要一个vtable,例如:

struct Base {
    Base();
    virtual void f() {}
    virtual void g() = 0;
};

void h(Base& b) {
    b.f(); // Call f on a Base that is not (yet) a Derived
           // vtable for Base required
}

Base::Base() {
    h(*this);
}

struct Derived : Base {
    void g() {}
};

int main() {
    Derived d;
}

)


我想看看它们如何被实现,而不使用虚拟表或任何其他方法,在其中至少必须向作为“起点”的类添加sizeof(void*)。 - user405725
1
@VladLazarenko:我认为几乎普遍认为基于vptr/vtable的解决方案是最优的,但是存储地址到某种形式的动态类型信息的映射等不切实际的实现至少在理论上是可能的。 - CB Bailey
你说得对。我的意思是,无论你如何称呼它,你都需要至少一个起点来访问它,可以是指向vtable的指针,指向地址哈希表的指针或哈希本身,或者是映射到函数指针的全局哈希表。 - user405725

2
vtable并非必要,但很少被优化删除。MSVC提供了__declspec(novtable)扩展,明确告诉编译器可以删除vtable。在没有这个的情况下,编译器必须自行检查vtable是否不被使用。虽然这并不特别困难,但仍然不是微不足道的。由于它在常规代码中并不能提供真正的速度优势,因此我所知道的任何编译器都没有实现这个检查。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接