When exactly does the virtual table pointer (in C++) gets set for an object?
我知道,对于任何具有虚函数的类或从具有虚函数的类派生的类,编译器都会做两件事。首先,它为该类创建一个虚拟表,其次,它在对象的基础部分放置一个虚拟指针(vptr)。在运行时,这个vptr被分配,并在对象被实例化时开始指向正确的vtable。
我的问题是,在实例化过程中,这个vptr是在哪里设置的?vptr的分配是否发生在构造函数之前/之后的对象的构造函数内?
- 它完全依赖于实现。
- @再也不会有猫了…关于vtable的所有问题都取决于定义实现。不需要指出重述显而易见的事实。
- 从技术上讲,它必须在执行构造函数之前设置指针,因为此时对象已经是该类型的对象(尽管尚未初始化)。但同样,标准根本不需要vtable,这只是实现虚拟功能的常见方式。此外,由于类型在构造函数中是静态已知的,因此可以静态解析从构造函数进行的虚拟函数调用。因此,实际上可以在之后初始化vtable(即使它是错误的,您也不会注意到)。
- @davidrodr&237;guez dribeas:我同意这个问题值得回答,它确实是一个有效的问题,但在声明这是实现定义时没有任何危害,因为许多新用户(相信我,我也是其中一个)真的不知道整个虚拟机制依赖于实现,对于那些转向从非计算机编程的人来说更是如此。科学背景。
- @达蒙:不是这样的,你可以把*this传递给一个引用你的基类型的函数,然后从那里调用虚函数。另一个函数内部的分派必须是动态的,因为编译器不知道调用者是谁。
- @艾尔斯:我想这取决于你是想要一种理论上的计算机科学方法,还是一种实际的方法。我不知道除了VTHT和VPTR之外,还有什么其他的方法被用于C++,并且在标准中规定的行为决定了VPTR如何被更新(如果这是动态调度的选择的解决方案)。是的,它不是由标准授权的,但同时实际上是独立于实现的,因为在这方面,所有实现都是相同的。
- @davidrodr&237;guez dribeas:我同意,从来没有见过编译器以任何其他方式实现动态调度。不久前,我对关于备用动态调度机制的非常相同的Q感兴趣,并在这里问了一个Q,因此,不确定您以前是否已经阅读过它,但您可能会发现内容很有趣。这里是一个关于虚拟调度的问题。C++中的机制(我承认Q标题真的很糟糕,那是我的早期)
- @Davidrodr&237;Guez Dribeas:啊,忽略链接,只看到你对上述线索中一些答案的评论。
- @艾尔斯:)我读过(忘记了)公认的答案…我真的很想知道语言可以是什么,以及它的实际表现如何。有相当多的人认为通过vtable进行虚拟调度对于紧凑的循环是低效的,并且通过调用方的内存地址查找vtable等价物的开销肯定是跟着vptr到vtable并从那里发送的开销的几倍…我不仅在考虑实际的处理器指令,而且在考虑数据的位置(在Judy树上行走可能会触发一些缓存未命中…)
这完全依赖于实现。
对于大多数编译器,
编译器在每个构造函数的成员初始值设定项列表中初始化这个->vptr。
其思想是使每个对象的v指针指向其类的v-table,编译器为此生成隐藏代码并将其添加到构造函数代码中。类似:
1 2 3 4 5
| Base::Base(...arbitrary params...)
: __vptr(&Base::__vtable[0]) ← supplied by the compiler, hidden from the programmer
{
} |
这个C++ FAQ解释了到底发生了什么。
- 这与使用vtable的选择一样依赖于实现。对于所有使用vtables(即all)的编译器,除了在每个构建/销毁级别更新vptr之外,别无选择。
- @davidrodr&237;guez dribeas:我同意,我所知道的所有编译器都使用vtable和vptr机制,并且应该使用这种机制,但是由于实现不受标准的约束,因此第一个语句是这样的。
- @Davidrodr&237;Guez Dribeas我不怪他添加了免责声明-如果你不这样做,太多人会绕过你的答案,甚至投反对票。
- 链接不再工作。
指向vtable的指针在层次结构中每个构造函数的条目上更新,然后在每个析构函数的条目上再次更新。vptr将开始指向基类,然后随着不同级别的初始化而更新。
虽然您将从许多不同的人那里了解到这是实现定义的,因为它是vtables的全部选择,但事实是所有编译器都使用vtables,并且一旦您选择vtable方法,标准确实规定运行时对象的类型是正在执行的构造函数/析构函数的类型,而这反过来意味着t不管动态调度机制是什么,它都必须随着构建/破坏链的遍历而调整。
请考虑以下代码段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <iostream>
struct base;
void callback( base const & b );
struct base {
base() { callback( *this ); }
~base() { callback( *this ); }
virtual void f() const { std::cout <<"base" << std::endl; }
};
struct derived : base {
derived() { callback( *this ); }
~derived() { callback( *this ); }
virtual void f() const { std::cout <<"derived" << std::endl; }
};
void callback( base const & b ) {
b.f();
}
int main() {
derived d;
} |
标准规定,该程序的输出为base、derived、derived、base,但callback中的调用与函数的所有四个调用相同。唯一可以实现它的方法是在构建/销毁过程中更新对象中的vptr。
这篇msdn的文章在Great Detali中解释了这一点。
上面写着:
"And the final answer is... as you'd expect. It happens in the constructor."
如果我可以在构造函数的开头添加,在您的构造函数中可能拥有的任何其他代码被执行之前。
但是要小心,假设您有一个类A,一个从A派生的类A1。
- 如果您创建一个新的对象,vptr将设置在类的构造函数的开头。
- 但如果创建新对象A1:
"Here's the entire sequence of events when you construct an instance of class A1:
A1::A1 calls A::A
A::A sets vtable to A's vtable
A::A executes and returns
A1::A1 sets vtable to A1's vtable
A1::A1 executes and returns"
在构造函数的主体中,可以调用虚拟函数,因此,如果实现使用了vptr,则该vptr已经设置。
请注意,在ctor中调用的虚拟函数是在该构造函数的类中定义的,而不是可能被更派生的类重写的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <iostream>
struct A
{
A() { foo (); }
virtual void foo () { std::cout <<"A::foo" << std::endl; }
};
struct B : public A
{
virtual void foo () { std::cout <<"B::foo" << std::endl; }
};
int
main ()
{
B b; // prints"A::foo"
b.foo (); // prints"B::foo"
return 0;
} |
- 根据ISO/IEC 14882:2011(e)[class.cdtor]12.7构造和破坏[4]"构件函数,包括虚拟函数(10.3),可以在构造或破坏过程中调用(12.6.2),在构造者中最明确地调用它们。"
- @pubby8:在这种特殊情况下,构造函数可以使用静态调度,但并非所有情况下都可以这样做。特别是,如果构造函数调用调度器函数并传递对A的引用,而调度器调用了虚函数,那么标准将强制执行相同的结果,在这种情况下,编译器不能使用静态调度,因为它在处理调用来自的调度器时不知道这一点。
虽然它是依赖于实现的,但它实际上必须在构造函数本身的主体被评估之前发生,因为您可以按照C++规范(12.7/3)允许通过构造函数主体中的EDCOX1×0指针来访问非静态类方法。因此,在调用construtor主体之前必须设置vtable,否则通过this指针调用虚拟类方法将无法正常工作。尽管EDCOX1、0指针和VTebe是两种不同的东西,但是C++标准允许EDCOX1×0指针在构造器的主体中使用这一事实说明编译器必须如何实现符合标准的EDCOX1×0指针正确使用的VTABLE,至少从时序角度来看是正确的。e.如果在调用构造函数主体期间或之后初始化vtable,那么使用this指针调用构造函数主体内的虚拟函数或将this指针传递给依赖动态调度的函数将是有问题的,并且会产生未定义的行为。
- 是的,但这只适用于this。请看我上面的评论。虚函数所属的类在构造函数中是已知的,不需要vtable来解析它们。
- vtable和this是两个完全不同的东西。
- @ PUBY8…完全同意,但只是说1)在构造函数体中,您可以访问非静态类方法;2)如果您通过this指针访问,并且vtable没有设置,那么当类inst的vtable调用虚拟函数时,您将遇到一些调用非静态虚拟函数的问题。ance this指针指向。
- @达蒙你上面的评论是错误的。你考虑一个特殊的情况(退化的情况),做出一个正确的陈述,然后将其概括。您对这些事实是正确的:(1)在构造函数的主体中,*this的动态类型由定义可知(2)对编译器已知其动态类型的对象的虚拟调用不需要使用vptr。就这些。您似乎认为在构造函数主体中有一个关于this的特殊规则,但是没有。
- 您可以混合特殊情况(可以优化)和一般情况。在特殊情况下,编译器可以避免在构造函数的开头设置vptr,因为它稍后将由派生类的构造函数设置。例如,当构造函数没有显式或隐式地使用this作为函数的参数;或者当函数是内联的等时,一般来说,构造函数可以以任何方式使用this。它是一个普通的指针,可以被复制并用于一般情况下不能"脱离"的虚拟调用(参见暂停问题)。
- @CuriousGuy:从ctor调用的任何虚拟函数都可以通过完全限定的名称(在多个继承和多个基类中具有相同名称的函数的情况下)调用,或者100%确定它属于哪个基类(名称要么唯一,要么只有一个基类),或者构建中断(调用不明确)。技术上甚至不需要这个指针,因为对象是静态的(不是动态的!)类型的定义是完全已知的,并且它完全知道必须调用哪个函数。这不是一个特殊情况,而是唯一可能的情况。
- @达蒙,你不知道你在说什么。合格电话在这里不相关。对虚拟函数的非限定调用使用动态类型period。
- @好奇的家伙:为什么最笨的人是最有侵略性的人……我不应该浪费时间讨论这个。从构造函数调用虚函数意味着从完全已知的类型调用完全已知的函数。要么因为只有一个可能的候选(单继承),要么因为您必须完全限定它(当多继承中存在多个不明确的名称时)。这不是动态调度,而是静态调度。当然,作为一个知道他在说什么的人,你知道。-)
- 达蒙,我在ISO C++委员会工作。有你?
- @好奇心的家伙:哦,哇,你是为ISO委员会工作的,是吗?我印象深刻。我将以链接到您在本网站上提出的几个问题结束讨论:stackoverflow.com/questions/32045888/&hellip;stackoverflow.com/questions/32100245/&hellip;stackoverflow.com/questions/32048698/&hellip;
- @达蒙,你能帮我回答这些问题吗?
- @好奇的家伙:你可以根据第3.7.4.3段(从你之前的声明中,你应该熟悉)自己回答他们。
- @达蒙,如果你认为"安全派生的指针"是我的一些问题的答案,请发表一个答案。
- @达蒙,你是说指针不是普通类型吗?