Vtables是否仅与指向基类的指针一起使用

Are Vtables only used with a pointer to base class

本文关键字:指针 一起 基类 是否 Vtables      更新时间:2023-10-16

我知道这里有很多关于vtables的问题,但我还是有点困惑。

vtables是否只有当我们有一个指向基类的指针来解析要调用派生类的哪个虚拟函数时才使用?

在我下面的例子中,在情况1中,即使Tiger对象不是在堆/空闲存储中动态创建的,在运行时这里是否使用vtables?

在情况2中,是否使用vtables,即使编译器在编译时知道我们指向的是Tiger对象。

案例3呢?

提前谢谢。

#include <iostream>
using namespace std;
class Animal // base class
{
    public:
        virtual void makeNoise() {cout<<"  "<<endl;}
};
class Tiger: public Animal
{
    public:
        void makeNoise() {cout<<"Tiger Noise"<<endl;}
};
class Elephant: public Animal
{
    public:
        void makeNoise() {cout<<"Elephant Noise"<<endl;}
};
int main()
{
    //case 1
    Tiger t1;
    Animal* aptr = &t1;
    aptr->makeNoise(); // vtables used?
    //case 2
    Tiger t2;
    Tiger* tptr = &t2;  //vtables used ?
    tptr->makeNoise();
    //case 3
    Elephant e1;       //vtables used ?
    e1.makeNoise();
}

特定编译器是使用虚拟函数表还是使用完全不同的机制来实现动态虚拟函数调度,取决于编译器的内部实现。如果您想了解特定编译器的行为,请参阅该编译器的文档和/或源代码。

C++语言本身定义了虚拟函数调用必须如何工作,并将其留给编译器来实现

该标准所要求的是,根据调用该函数的对象的动态类型,将虚拟函数的调用调度到最终的覆盖器。在您的代码中,t1t2的动态类型是Tigere1的动态类型为Elephant

是的,大多数(如果不是全部的话)编译器都使用虚拟函数表来实现虚拟函数调用。是的,如果有能力,任何优秀的编译器都应该最大限度地尝试在编译时解决动态调度问题,并在可能的情况下用直接调用取代虚拟表的使用(这是编译器的实现质量问题)。

在您的例子中,哪一个调用将被静态调度取决于编译器的优化器的"侵略性"(或者"智能性",如果您愿意的话)

我想说的是,每个健全的编译器都应该通过e1静态调度调用,即使禁用了优化。调用那里的动态调度机制是完全不必要的。

至于通过aptrtptr的调用,这取决于编译器优化器的静态分析器是否能够消除aptrtptr,并使用它们所指向的实际对象来替换它们(因为该信息在编译时可用)。一个好的优化器应该能够做到这一点,并静态地调度所有3个调用。

要确定编译器如何处理调用,请检查生成的程序集。

正如其他评论所说,vtables的使用由编译器处理,只要生成的输出是预期的,编译器就可以尝试优化它们的访问。

然而,我们可能会将vtables视为包含虚拟方法地址的表。对父类中已声明为"虚拟"的方法的每次调用都应该在运行时检查vtable,以便知道要跳转的特定地址。

这是程序员所期望的行为,尽管特定的机制可能更棘手,如果编译器可以在编译时确定地址,甚至可能根本不依赖于查询vtables。

因此,在所有这些情况下,编译器可能足够聪明,可以在编译时设置地址。但你应该相信,在"最坏的情况"下,在每种情况下都会访问vtable,因为你调用的是虚拟方法-这是预期的行为-并让编译器进行它认为必须进行的优化。

正如您在情况1中所说的那样,vtable访问与对象是在堆中还是在堆栈中分配无关。这些都是完全不同的概念。

看到它们是如何编译的,我很感兴趣。这就是我所看到的:

Clang 13和GCC 11.2产生了相同的结果。我在下面提到的组件来自Clang:

无优化:

case 1
aptr->makeNoise(); 
assembly: call    qword ptr [rax]
Call to pointer, therefore, vtable is used
case 2
tptr->makeNoise();
assembly: call    qword ptr [rax]
Call to pointer, therefore, vtable is used

//case 3
e1.makeNoise();
assembly: call    Elephant::makeNoise()
Direct call

-O1 Clang

case 1
aptr->makeNoise(); 
assembly: call    Tiger::makeNoise()
direct call
case 2
tptr->makeNoise();
assembly: call    Tiger::makeNoise()
direct call
case 3
e1.makeNoise();
assembly: call    Elephant::makeNoise()
Direct call

进一步的优化将使-O1更加优化。