Vtables是否仅与指向基类的指针一起使用
Are Vtables only used with a pointer to base class
我知道这里有很多关于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++语言本身定义了虚拟函数调用必须如何工作,并将其留给编译器来实现
该标准所要求的是,根据调用该函数的对象的动态类型,将虚拟函数的调用调度到最终的覆盖器。在您的代码中,t1
和t2
的动态类型是Tiger
,e1
的动态类型为Elephant
。
是的,大多数(如果不是全部的话)编译器都使用虚拟函数表来实现虚拟函数调用。是的,如果有能力,任何优秀的编译器都应该最大限度地尝试在编译时解决动态调度问题,并在可能的情况下用直接调用取代虚拟表的使用(这是编译器的实现质量问题)。
在您的例子中,哪一个调用将被静态调度取决于编译器的优化器的"侵略性"(或者"智能性",如果您愿意的话)
我想说的是,每个健全的编译器都应该通过e1
静态调度调用,即使禁用了优化。调用那里的动态调度机制是完全不必要的。
至于通过aptr
和tptr
的调用,这取决于编译器优化器的静态分析器是否能够消除aptr
和tptr
,并使用它们所指向的实际对象来替换它们(因为该信息在编译时可用)。一个好的优化器应该能够做到这一点,并静态地调度所有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
更加优化。
- 在将 new 与指针一起使用时,创建数组的指定长度
- 为什么我们将 [ ] 与指向数组的指针一起使用?
- 如何将数组与指针一起使用?
- 如何将 string.erase() 与指针一起使用?
- __unaligned说明符何时与指针一起使用?
- 为什么在将多态行为与指向接口的指针一起使用时没有调用析构函数?
- 自动关键字与智能指针一起
- shared_ptr ::重置仅与原始指针一起使用
- 为什么动态演员只能与参考和指针一起使用
- 是否可以将SDL2与智能指针一起使用
- 如何将此指针与指向成员函数的指针一起使用
- 将shared_ptr与指向指针的指针一起使用时出现编译器错误
- 将 std::sort 与指向整数变量的指针一起使用会产生意外的输出
- QtConcurrent::run 是否可以与指向对象的智能指针一起使用
- 重新分配指针后,将类功能与指针一起使用
- 将共享指针的别名构造函数与空共享指针一起使用C++
- 如何将转换构造函数与指针一起使用?
- 如何 RAII 与指针一起使用
- Vtables是否仅与指向基类的指针一起使用
- 将 decltype 与虚拟成员函数指针一起使用