[英]I get why, but how exactly do Virtual Functions/VTables allow the correct functions to be accessed through pointers?
[英]How do upcasting and vtables work together to ensure correct dynamic binding?
因此, vtable
是由编译器维护的表,其中包含指向该类中的虚函数的函数指针。
和
将派生类的对象分配给祖先类的对象称为向上转换。
向上转换使用基类指针或引用处理派生类实例/对象; 对象不是“赋值给”,这意味着覆盖值ala operator = invocation。
(感谢: Tony D )
现在,如何在运行时知道“哪个”类的虚函数应该被调用?
vtable中的哪个条目指的是应该在运行时调用的“特定”派生类的功能?
您可以想象(尽管C ++规范没有说明这一点)vtable是一个标识符(或一些其他可用于“查找有关类本身的更多信息”的元数据)和一系列函数。
所以,如果我们有这样一个类:
class Base
{
public:
virtual void func1();
virtual void func2(int x);
virtual std::string func3();
virtual ~Base();
... some other stuff we don't care about ...
};
然后编译器将生成这样的VTable:
struct VTable_Base
{
int identifier;
void (*func1)(Base* this);
void (*func2)(Base* this, int x);
std::string (*func3)(Base* this);
~Base(Base *this);
};
然后编译器将创建一个类似于此的内部结构(这不可能作为C ++编译,它只是为了显示编译器实际执行的操作 - 我称之为Sbase
以区分实际的class Base
)
struct SBase
{
VTable_Base* vtable;
inline void func1(Base* this) { vtable->func1(this); }
inline void func2(Base* this, int x) { vtable->func2(this, x); }
inline std::string func3(Base* this) { return vtable->func3(this); }
inline ~Base(Base* this) { vtable->~Base(this); }
};
它还构建了真正的vtable:
VTable_Base vtable_base =
{
1234567, &Base::func1, &Base::func2, &Base::func3, &Base::~Base
};
在Base
的构造函数中,它将设置vtable = vtable_base;
。
当我们添加派生类时,我们覆盖一个函数(默认情况下,析构函数,即使我们没有声明一个):
class Derived : public Base
{
virtual void func2(int x) override;
};
编译器现在将构建此结构:
struct VTable_Derived
{
int identifier;
void (*func1)(Base* this);
void (*func2)(Base* this, int x);
std::string (*func3)(Base* this);
~Base(Derived *this);
};
然后做同样的“结构”建设:
struct SDerived
{
VTable_Derived* vtable;
inline void func1(Base* this) { vtable->func1(this); }
inline void func2(Base* this, int x) { vtable->func2(this, x); }
inline std::string func3(Base* this) { return vtable->func3(this); }
inline ~Derived(Derived* this) { vtable->~Derived(this); }
};
当我们直接使用Derived
而不是通过Base
类时,我们需要这个结构。
(我们依赖编译器链~Derived
来调用~Base
,就像继承的普通析构函数一样)
最后,我们建立一个实际的vtable:
VTable_Derived vtable_derived =
{
7654339, &Base::func1, &Derived::func2, &Base::func3, &Derived::~Derived
};
同样, Derived
构造Dervied::vtable = vtable_derived
将为所有实例设置Dervied::vtable = vtable_derived
。
编辑以回答评论中的问题:编译器必须小心地将各种组件放在VTable_Derived
和SDerived
,以便它匹配VTable_Base
和SBase
,这样当我们有一个指向Base
的指针时, Base::vtable
和Base::funcN()
匹配Derived::vtable
和Derived::FuncN
。 如果不匹配,那么继承将不起作用。
如果将新虚拟函数添加到Derived
,则必须将它们放在从Base
继承的函数之后。
结束编辑。
所以,当我们这样做时:
Base* p = new Derived;
p->func2();
该代码将查找SBase::Func2
,其将使用正确的Derived::func2
(因为实际的vtable
内p->vtable
是VTable_Derived
(由设置Derived
的构造被称为结合new Derived
)。
我将采取与其他答案不同的路线,并尝试填补您的知识中的具体差距,而不必深入细节。 我会解决这些机制,足以帮助你理解。
因此,vtable是由编译器维护的表,其中包含指向该类中的虚函数的函数指针。
更准确的说法如下:
每个具有虚方法的类,包括从具有虚方法的类继承的每个类,都有自己的虚拟表。 类的虚拟表指向特定于该类的虚方法,即继承方法,重写方法或新添加的方法。 这样一个类的每个实例都包含一个指向与该类匹配的虚拟表的指针。
向上转换使用基类指针或引用处理派生类实例/对象; (......)
也许更具启发性:
向上转换意味着对Derived
类的实例的指针或引用被视为指向类Base
的实例的指针或引用。 然而,实例本身仍然纯粹是Derived
一个实例。
(当指针被“视为指向Base
的指针”时,这意味着编译器生成用于处理指向Base
的指针的代码。换句话说,编译器和生成的代码并不比它们处理指针更好到Base
,因此,被“当作”指针会指向一个对象,它至少提供了相同的接口的实例Base
,这恰好是对的情况下Derived
因为继承的。我们将会看到这是如何在下面工作。)
此时我们可以回答您问题的第一个版本。
现在,如何在运行时知道“哪个”类的虚函数应该被调用?
假设我们有一个指向Derived
实例的指针。 首先我们将它上传,因此它被视为指向Base
实例的指针。 然后我们在我们的upcasted指针上调用一个虚方法。 由于编译器知道该方法是虚拟的,因此它知道在实例中查找虚拟表指针。 虽然我们将指针视为指向Base
的实例,但实际对象没有更改值,并且其中的虚拟表指针仍指向Derived
的虚拟表。 因此,在运行时,方法的地址取自Derived
的虚拟表。
现在,特定方法可以从Base
继承,也可以在Derived
覆盖。 不要紧; 如果继承,则Derived
虚拟表中的方法指针只包含与Base
虚拟表中相应方法指针相同的地址。 换句话说,两个表都指向该特定方法的相同方法实现。 如果被覆盖, Derived
虚拟表中的方法指针与Base
的虚拟表中的相应方法指针不同,因此Derived
实例上的方法查找将找到重写方法,而对Base
实例的查找将找到原始版本的方法 - 无论指向实例的指针是否被视为指向Base
的指针或指向Derived
的指针。
最后,现在应该直截了当地解释为什么你的问题的第二个版本有点误导:
vtable中的哪个条目指的是应该在运行时调用的“特定”派生类的功能?
这个问题预先假定vtable查找首先是方法,然后是类。 反过来说:首先,实例中的vtable指针用于查找正确类的vtable。 然后,该类的vtable用于查找正确的方法。
vtable中的哪个条目指的是应该在运行时调用的“特定”派生类的功能?
无,它不是vtable中的条目,而是vtable指针,它是每个对象实例的一部分,用于确定哪个是该特定对象的正确虚函数集。 这样,根据指向的实际vtable,从vtable调用“第一虚拟方法”可能导致在同一多态层次结构中为不同类型的对象调用不同的函数。
实现可能会有所不同,但我个人认为最合乎逻辑且最常执行的事情是将vtable指针作为类布局中的第一个元素。 这样,您可以取消引用对象的地址,以根据位于该地址中的指针的值确定其类型,因为给定类型的所有对象都将指向同一个vtable,该vtable是为每个vtable创建的具有虚拟方法的对象,这是启用功能以覆盖某些虚拟方法所必需的。
upcasting和vtables如何协同工作以确保正确的动态绑定?
不是严格要求向上倾斜,也不是向下倾斜。 请记住,您已经在内存中分配了对象,并且它已经将其vtable指针设置为该类型的正确vtable,这样可以确保它,向下转换不会更改该对象的vtable,它只会更改你操作的指针。
当您想要访问基类中不可用的功能并在派生类中声明时,需要向下转换。 但是在尝试执行此操作之前,必须确保特定对象是或继承声明该功能的类型,即dynamic_cast
所在的类型,当动态转换编译器生成对该vtable条目的检查以及是否继承来自另一个表的请求类型,在编译时生成,如果是,则动态转换成功,否则失败。
您访问对象的指针并不是指要调用的正确的虚函数集,它仅用作衡量vtable中您可以称为开发人员的函数。 这就是为什么使用C样式或静态强制转换进行向上转换是安全的,它不执行运行时检查,因为这样你只能将量程限制为基类中可用的函数,这些函数已在派生类中可用,因此有没有错误和伤害的余地。 这就是为什么在向下转换时必须始终使用动态强制转换或其他一些仍基于虚拟分派的自定义技术,因为您必须确保对象的关联vtable确实包含您可能调用的额外功能。
否则你会得到未定义的行为,以及那种“坏的”,这意味着最有可能发生致命的事情,因为将任意数据解释为要调用的特定签名函数的地址是一个非常大的禁忌。
还要注意,在静态上下文中,即在编译时知道类型是什么时,编译器很可能不会使用vtable来调用虚函数,而是使用直接静态调用甚至内联某些函数,这将使它们成为快多了。 在这种情况下,向上转换并使用基类指针而不是实际对象只会减少该优化。
铸造铸造是与变量相关的概念。 所以任何变量都可以铸造。 它可以向上或向下铸造。
char charVariable = 'A';
int intVariable = charVariable; // upcasting
int intVariable = 20;
char charVariale = intVariable; // downcasting
对于系统定义的数据类型向上转换或向下转换基于您当前的变量,它主要与内存编译器分配给两个比较变量的内存有关。
如果要分配的变量分配的内存少于要转换的类型,则调用up cast。
如果要分配的变量分配的内存多于转换的类型,则称为向下转换。 当试图转换的值无法适应分配的内存区域时,向下转换会产生一些问题。
类级别的上传与系统定义的数据类型一样,我们可以拥有基类和派生类的对象。 因此,如果我们想将派生类型转换为基类型,则称为向下向上转换。 这可以通过指向派生类类型的基类的指针来实现。
class Base{
public:
void display(){
cout<<"Inside Base::display()"<<endl;
}
};
class Derived:public Base{
public:
void display(){
cout<<"Inside Derived::display()"<<endl;
}
};
int main(){
Base *baseTypePointer = new Derived(); // Upcasting
baseTypePointer.display(); // because we have upcasted we want the out put as Derived::display() as output
}
Inside Base :: display()
里面的Derived :: display()
在上面的场景中,输出并不是例外。 因为我们在对象中没有v-table和vptr(虚拟指针),所以基本指针将调用Base :: display(),尽管我们已将派生类型分配给基指针。
为了避免这个问题,c ++为我们提供了虚拟概念。 现在需要将基类显示功能更改为虚拟类型。
virtual void display()
完整代码是:
class Base{
public:
virtual void display(){
cout<<"Inside Base::display()"<<endl;
}
};
class Derived:public Base{
public:
void display(){
cout<<"Inside Derived::display()"<<endl;
}
};
int main(){
Base *baseTypePointer = new Derived(); // Upcasting
baseTypePointer.display(); // because we have upcasted we want the out put as Derived::display() as output
}
里面的Derived :: display()
里面的Derived :: display()
要理解这一点,我们需要了解v-table和vptr; 当编译器找到一个虚函数和一个函数时,它将为每个类(Base和所有派生类)生成一个虚拟表。
如果存在虚函数,则每个对象将包含指向相应类vtable的vptr(虚拟指针),而vtable将包含指向相应类虚函数的指针。 当你通过vptr调用函数时,将调用virutal函数并调用相应的类函数,我们将实现所需的输出。
解释跨模块边界可见的抽象类型及其行为需要一个通用的应用程序二进制接口(ABI)。 当然,C ++标准不需要实现任何特定的ABI。
ABI会描述:
以下示例中的两个模块external.so
和main.o
都假定已链接到同一运行时。 静态和动态绑定优先考虑位于调用模块内的符号。
external.h(分发给用户):
class Base
{
__vfptr_t __vfptr; // For exposition
public:
__attribute__((dllimport)) virtual int Helpful();
__attribute__((dllimport)) virtual ~Base();
};
class Derived : public Base
{
public:
__attribute__((dllimport)) virtual int Helpful() override;
~Derived()
{
// Visible destructor logic here.
// Note: This is in the header!
// __vft@Base gets treated like any other imported symbol:
// The address is resolved at load time.
//
this->__vfptr = &__vft@Base;
static_cast<Base *>(this)->~Base();
}
};
__attribute__((dllimport)) Derived *ReticulateSplines();
external.cpp:
#include "external.h" // the version in which the attributes are dllexport
__attribute__((dllexport)) int Base::Helpful()
{
return 47;
}
__attribute__((dllexport)) Base::~Base()
{
}
__attribute__((dllexport)) int Derived::Helpful()
{
return 4449;
}
__attribute__((dllexport)) Derived *ReticulateSplines()
{
return new Derived(); // __vfptr = &__vft@Derived in external.so
}
external.so(不是真正的二进制布局):
__vft@Base:
[offset to __type_info@Base] <-- in external.so
[offset to Base::~Base] <------- in external.so
[offset to Base::Helpful] <----- in external.so
__vft@Derived:
[offset to __type_info@Derived] <-- in external.so
[offset to Derived::~Derived] <---- in external.so
[offset to Derived::Helpful] <----- in external.so
Etc...
__type_info@Base:
[null base offset field]
[offset to mangled name]
__type_info@Derived:
[offset to __type_info@Base]
[offset to mangled name]
Etc...
special.hpp:
#include <iostream>
#include "external.h"
class Special : public Base
{
public:
int Helpful() override
{
return 55;
}
virtual void NotHelpful()
{
throw std::exception{"derp"};
}
};
class MoreDerived : public Derived
{
public:
int Helpful() override
{
return 21;
}
~MoreDerived()
{
// Visible destructor logic here
this->__vfptr = &__vft@Derived; // <- the version in main.o
static_cast<Derived *>(this)->~Derived();
}
};
class Related : public Base
{
public:
virtual void AlsoHelpful() = 0;
};
class RelatedImpl : public Related
{
public:
void AlsoHelpful() override
{
using namespace std;
cout << "The time for action... Is now!" << endl;
}
};
main.cpp中:
#include "special.hpp"
int main(int argc, char **argv)
{
Base *ptr = new Base(); // ptr->__vfptr = &__vft@Base (in external.so)
auto r = ptr->Helpful(); // calls "Base::Helpful" in external.so
// r = 47
delete ptr; // calls "Base::~Base" in external.so
ptr = new Derived(); // ptr->__vfptr = &__vft@Derived (in main.o)
r = ptr->Helpful(); // calls "Derived::Helpful" in external.so
// r = 4449
delete ptr; // calls "Derived::~Derived" in main.o
ptr = ReticulateSplines(); // ptr->__vfptr = &__vft@Derived (in external.so)
r = ptr->Helpful(); // calls "Derived::Helpful" in external.so
// r = 4449
delete ptr; // calls "Derived::~Derived" in external.so
ptr = new Special(); // ptr->__vfptr = &__vft@Special (in main.o)
r = ptr->Helpful(); // calls "Special::Helpful" in main.o
// r = 55
delete ptr; // calls "Base::~Base" in external.so
ptr = new MoreDerived(); // ptr->__vfptr = & __vft@MoreDerived (in main.o)
r = ptr->Helpful(); // calls "MoreDerived::Helpful" in main.o
// r = 21
delete ptr; // calls "MoreDerived::~MoreDerived" in main.o
return 0;
}
main.o:
__vft@Derived:
[offset to __type_info@Derivd] <-- in main.o
[offset to Derived::~Derived] <--- in main.o
[offset to Derived::Helpful] <---- stub that jumps to import table
__vft@Special:
[offset to __type_info@Special] <-- in main.o
[offset to Base::~Base] <---------- stub that jumps to import table
[offset to Special::Helpful] <----- in main.o
[offset to Special::NotHelpful] <-- in main.o
__vft@MoreDerived:
[offset to __type_info@MoreDerived] <---- in main.o
[offset to MoreDerived::~MoreDerived] <-- in main.o
[offset to MoreDerived::Helpful] <------- in main.o
__vft@Related:
[offset to __type_info@Related] <------ in main.o
[offset to Base::~Base] <-------------- stub that jumps to import table
[offset to Base::Helpful] <------------ stub that jumps to import table
[offset to Related::AlsoHelpful] <----- stub that throws PV exception
__vft@RelatedImpl:
[offset to __type_info@RelatedImpl] <--- in main.o
[offset to Base::~Base] <--------------- stub that jumps to import table
[offset to Base::Helpful] <------------- stub that jumps to import table
[offset to RelatedImpl::AlsoHelpful] <-- in main.o
Etc...
__type_info@Base:
[null base offset field]
[offset to mangled name]
__type_info@Derived:
[offset to __type_info@Base]
[offset to mangled name]
__type_info@Special:
[offset to __type_info@Base]
[offset to mangled name]
__type_info@MoreDerived:
[offset to __type_info@Derived]
[offset to mangled name]
__type_info@Related:
[offset to __type_info@Base]
[offset to mangled name]
__type_info@RelatedImpl:
[offset to __type_info@Related]
[offset to mangled name]
Etc...
根据方法和绑定方面可以证明的内容,可以静态或动态地绑定虚方法调用。
动态虚方法调用将从__vfptr
成员指向的vtable中读取目标函数的地址。
ABI描述了如何在vtable中排序函数。 例如:它们可能按类排序,然后按字典顺序排列,包括错误的名称(包括有关常量,参数等的信息......)。 对于单继承,这种方法保证函数的虚拟调度索引始终是相同的,无论有多少不同的实现。
在此处给出的示例中,如果适用,将析构函数放置在每个vtable的开头。 如果析构函数是微不足道的和非虚拟的(未定义或什么也不做),编译器可能会完全忽略它,而不是为它分配vtable条目。
Base *ptr = new Special{};
MoreDerived *md_ptr = new MoreDerived{};
// The cast below is checked statically, which would
// be a problem if "ptr" weren't pointing to a Special.
//
Special *sptr = static_cast<Special *>(ptr);
// In this case, it is possible to
// prove that "ptr" could point only to
// a Special, binding statically.
//
ptr->Helpful();
// Due to the cast above, a compiler might not
// care to prove that the pointed-to type
// cannot be anything but a Special.
//
// The call below might proceed as follows:
//
// reg = sptr->__vptr[__index_of@Base::Helpful] = &Special::Helpful in main.o
//
// push sptr
// call reg
// pop
//
// This will indirectly call Special::Helpful.
//
sptr->Helpful();
// No cast required: LSP is satisfied.
ptr = md_ptr;
// Once again:
//
// reg = ptr->__vfptr[__index_of@Base::Helpful] = &MoreDerived::Helpful in main.o
//
// push ptr
// call reg
// pop
//
// This will indirectly call MoreDerived::Helpful
//
ptr->Helpful();
对于需要动态绑定的任何调用站点,上述逻辑都是相同的。 在上面的例子中, ptr
或sptr
指向的确切类型sptr
; 代码只是将指针加载到已知的偏移量,然后盲目地调用它。
在转换强制转换或函数调用表达式时,编译器必须可以使用有关类型层次结构的所有信息。 象征性地,铸造只是遍历有向图的问题。
这个简单的ABI中的向上转换可以在编译时完全执行。 编译器只需要检查类型层次结构以确定源类型和目标类型是否相关(在类型图中存在从源到目标的路径)。 通过替换原则 ,指向MoreDerived
的指针也指向Base
并且可以解释为这样。 __vfptr
成员对于此层次结构中的所有类型具有相同的偏移量,因此RTTI逻辑不需要处理任何特殊情况(在VMI的某些实现中,它需要从类型thunk中获取另一个偏移量以获取另一个vptr和等......)
然而,降级是不同的。 由于从基类型转换为派生类型涉及确定指向对象是否具有兼容的二进制布局,因此必须执行显式类型检查(从概念上讲,这是“证明”额外信息存在于结束之外在编译时假设的结构)。
请注意, Derived
类型有多个vtable实例:一个在external.so
,另一个在main.o
。 这是因为为Derived
(其析构函数)定义的虚方法出现在包含external.h
每个翻译单元中。
尽管两种情况下的逻辑都相同,但本例中的两个图像都需要有自己的副本。
然后通过从运行时解码的源类型开始遍历类型图(在两个图像中复制)来执行向下转换,比较错位名称直到编译时目标匹配。
例如:
Base *ptr = new MoreDerived();
// ptr->__vfptr = &__vft::MoreDerived in main.o
//
// This provides the code below with a starting point
// for dynamic cast graph traversals.
// All searches start with the type graph in the current image,
// then all other linked images, and so on...
// This example is not exhaustive!
// Starts by grabbing &__type_info@MoreDerived
// using the offset within __vft@MoreDerived resolved
// at load time.
//
// This is similar to a virtual method call: Just grab
// a pointer from a known offset within the table.
//
// Search path:
// __type_info@MoreDerived (match!)
//
auto *md_ptr = dynamic_cast<MoreDerived *>(ptr);
// Search path:
// __type_info@MoreDerived ->
// __type_info@Derived (match!)
//
auto *d_ptr = dynamic_cast<Derived *>(ptr);
// Search path:
// __type_info@MoreDerived ->
// __type_info@Derived ->
// __type_info@Base (no match)
//
// Did not find a path connecting RelatedImpl to MoreDerived.
//
// rptr will be nullptr
//
auto *rptr = dynamic_cast<RelatedImpl *>(ptr);
在上面的代码中没有任何一点需要改变ptr->__vfptr
。 C ++中类型推导的静态性质要求实现在编译时满足替换原则,这意味着对象的实际类型不能在运行时更改。
我已经将这个问题理解为动态调度背后的机制。
对我来说, “vtable中的哪个条目指的是应该在运行时调用的”特定“派生类的功能?” ,问的是vtable是如何工作的。
这个答案旨在证明类型转换只影响对象数据的视图,并且这些示例中的动态分派的实现独立于它运行。 但是,在多重继承的情况下,类型转换会影响动态分派,其中确定要使用哪个 vtable可能需要多个步骤(具有多个基础的类型的实例可能具有多个vptrs)。
让我试着用一些例子解释一下: -
class Base
{
public:
virtual void function1() {cout<<"Base :: function1()\n";};
virtual void function2() {cout<<"Base :: function2()\n";};
virtual ~Base(){};
};
class D1: public Base
{
public:
~D1(){};
virtual void function1() { cout<<"D1 :: function1()\n";};
};
class D2: public Base
{
public:
~D2(){};
virtual void function2() { cout<< "D2 :: function2\n";};
};
因此,编译器将为每个类生成一个vtable,因为这些类具有虚函数。 (虽然它依赖于编译器)。
注意: - vtables仅包含指向虚函数的指针。 非虚函数仍然可以在编译时解决...
你是对的,说vtable不仅仅是指向函数的指针。 这些类的vtables就像是: -
基地vtable: -
&Base::function1 ();
&Base::function2 ();
&Base::~Base ();
D1的vtable: -
&D1::function1 ();
&Base::function2 ();
&D1::~D1();
D2的vtable: -
&Base::function1 ();
&D2::function2 ();
&D2::~D2 ();
vptr是一个指针,用于在此表上查找目的。 多态类的每个对象都有额外的vptr空间(尽管vptr在对象中的位置完全取决于实现)。一般来说,vptr位于对象的开头。
考虑到所有因素,如果我调用func,编译器在运行时将检查b实际指向的是什么: -
void func ( Base* b )
{
b->function1 ();
b->function2 ();
}
假设我们将D1的对象传递给func。 编译器将按以下方式解析呼叫: -
首先,它将从对象获取vptr,然后它将使用它来获取要调用的函数的正确地址。 因此,在这种情况下,vptr将允许访问D1的vtable。 当它查找function1时,它将获得在基类中定义的function1的地址。 在调用function2的情况下,它将获得base的function2的地址。
希望我已经澄清了你的疑虑让你满意......
我相信,最好通过在C中实现多态来解释这个。给出这两个C ++类:
class Foo {
virtual void foo(int);
};
class Bar : public Foo {
virtual void foo(int);
virtual void bar(double);
};
C结构定义(即头文件)如下所示:
//For class Foo
typedef struct Foo_vtable {
void (*foo)(int);
} Foo_vtable;
typedef struct Foo {
Foo_vtable* vtable;
} Foo;
//For class Bar
typedef struct Bar_vtable {
Foo_vtable super;
void (*bar)(double);
}
typedef struct Bar {
Foo super;
} Bar;
如您所见,每个类有两个结构定义,一个用于vtable,另一个用于类本身。 还要注意, class Bar
两个结构都包含一个基类对象作为它们允许我们向上转换的第一个成员: (Foo*)myBarPointer
和(Foo_vtable*)myBar_vtablePointer
都是有效的。 因此,给定一个Foo*
,通过这样做可以安全地找到foo()
成员的位置
Foo* basePointer = ...;
(basePointer->vtable->foo)(7);
现在,让我们来看看我们如何实际填充vtable。 为此,我们编写了一些使用静态定义的vtable实例的构造函数,这就是foo.c文件的样子
#include "..."
static void foo(int) {
printf("Foo::foo() called\n");
}
Foo_vtable vtable = {
.foo = &foo,
};
void Foo_construct(Foo* me) {
me->vtable = vtable;
}
这确保了可以对已传递给Foo_construct()
每个对象执行(basePointer->vtable->foo)(7)
Foo_construct()
。 现在, Bar
的代码非常相似:
#include "..."
static void foo(int) {
printf("Bar::foo() called\n");
}
static void bar(double) {
printf("Bar::bar() called\n");
}
Bar_vtable vtable = {
.super = {
.foo = &foo
},
.bar = &bar
};
void Bar_construct(Bar* me) {
Foo_construct(&me->super); //construct the base class.
(me->vtable->foo)(7); //This will print Foo::foo()
me->vtable = vtable;
(me->vtable->foo)(7); //This will print Bar::foo()
}
我已经为成员函数使用了静态声明,以避免为每个实现创建一个新名称, static void foo(int)
限制了函数对源文件的可见性。 但是,仍然可以通过使用函数指针从其他文件中调用它。
这些类的用法可能如下所示:
#include "..."
int main() {
//First construct two objects.
Foo myFoo;
Foo_construct(&myFoo);
Bar myBar;
Bar_construct(&myBar);
//Now make some pointers.
Foo* pointer1 = &myFoo, pointer2 = (Foo*)&myBar;
Bar* pointer3 = &myBar;
//And the calls:
(pointer1->vtable->foo)(7); //prints Foo::foo()
(pointer2->vtable->foo)(7); //prints Bar::foo()
(pointer3->vtable->foo)(7); //prints Bar::foo()
(pointer3->vtable->bar)(7.0); //prints Bar::bar()
}
一旦你知道它是如何工作的,你就知道C ++ vtable是如何工作的。 唯一的区别是在C ++中编译器完成了我在上面的代码中完成的工作。
该实现是特定于编译器的。 在这里,我将做一些关于在编译器中如何完成它的任何实际知识无关的想法,但只是为了按要求工作所需的一些最低要求。 请记住,具有虚方法的类的每个实例在运行时都知道它所属的类。
让我们假设我们有一个长度为10的基类和派生类的链(因此派生类有一个gran gran ... gran父亲)。 我们可以将这些类称为base0 base1 ... base9,其中base9派生自base8等。
这些类中的每一个都将方法定义为:virtual void doit(){...}
让我们假设在基类中,我们在任何派生类中未被覆盖的名为“dowith_doit”的方法中使用该方法。 c ++的语义意味着,根据我们手头的实例的基类,我们必须在该实例中应用手头实例的基类中定义的“doit”。
基本上我们有两种可能的方法:a)为任何这样的虚方法分配一个数字,该数字对于派生类链中定义的每个方法必须是不同的。 在这种情况下,该数字也可以是方法名称的散列。 每个类定义一个包含2列的表,第一列包含方法的编号,第二列包含函数的地址。 在这种情况下,每个类都有一个vtable,其行数与在类中定义的虚拟方法的数量相同。 通过在类中搜索所考虑的方法来执行该方法。 该搜索可以通过二分(当存在基于方法的数量的顺序时)线性地(缓慢地)完成。
b)为任何此类方法分配逐步增加的整数(对于类链中的每个不同方法),并为每个类定义一个只有一列的表。 对于在类中定义的虚方法,函数地址将是由方法编号定义的raw。 将有许多带有空指针的行,因为每个类都不会覆盖以前类的方法。 实现可以选择以提高效率以使用所考虑的类的祖先类中的地址保持来填充空行。
基本上没有其他简单方法可以有效地使用虚拟方法。
我认为在实际实现中只使用第二个解决方案(b),因为用于非现有方法的空间开销与情况(b)的执行效率之间的交易对于情况b是有利的(同时考虑到方法是数量有限 - 可能是10 20 50但不是5000)。
在实例化时,具有至少一个虚函数的每个类获得通常称为vTable(或虚拟调度表,VDT)的隐藏成员。
class Base {
hidden: // not part of the language, just to illustrate.
static VDT baseVDT; // per class VDT for base
VDT *vTable; // per object instance
private:
...
public:
virtual int base1();
virtual int base2();
...
};
vTable包含指向Base中所有函数的指针。
作为Base的构造函数的隐藏部分,vTable被分配给baseVDT。
VDT Base::baseVDT[] = {
Base::base1,
Base::base2
};
class Derived : public Base {
hidden:
static VDT derivedVDT; // per class VDT for derived
private:
...
public:
virtual int base2();
...
};
用于Derived的vTable包含指向Base中定义的所有函数的指针,后跟Derived中定义的函数。 构造Derived类型的对象时,vTable将设置为derivedVDT。
VDT derived::derivedVDT[] = {
// functions first defined in Base
Base::base1,
Derived::base2, // override
// functions first defined in Derived are appended
Derived::derived3
}; // function 2 has an override in derived.
现在,如果我们有
Base *bd = new Derived;
Derived *dd = new Derived;
Base *bb = new Base;
bd
指向派生类型的对象,其vTable指向Derived
所以函数调用
x = bd->base2();
y = bb->base2();
实际上是
// "base2" here is the index into vTable for base2.
x = bd->vTable["base2"](); // vTable points to derivedVDT
y = bb->vTable["base2"](); // vTable points to baseVDT
由于VDT的构建,两者的指数相同。 这也意味着编译器在编译时知道索引。
这也可以实现为
// call absolute address to virtual dispatch function which calls the right base2.
x = Base::base2Dispatch(bd->vTable["base2"]);
inline Base::base2Dispatch(void *call) {
return call(); // call through function pointer.
}
与O2或O3相同。
有一些特殊情况:
dd指向派生的或更多派生的对象,然后base2被声明为final
z = dd->base2();
实际上是
z = Derived::base2(); // absolute call to final method.
如果dd指向Base对象或其他任何未定义的行为,编译器仍然可以执行此操作。
另一种情况是,如果编译器发现只有少数来自Base的派生类,它可以为base2生成Oracle接口。 [在2012年或2013年的某个C ++大会上,MS或英特尔编译人员可以免费使用? 显示(~500%?)更多代码平均加速(2倍以上?)加速
inline Base::base2Dispatch(void *call) {
if (call == Derived::base2) // most likely from compilers static analysis or profiling.
return Derived::base2(); // call absolute address
if (call == Base::base2)
return Base::base2(); // call absolute address
// Backup catch all solution in case of more derived classes
return call(); // call through function pointer.
}
你为什么要这样做呢? 更多的代码是坏的,不需要的分支会降低性能!
因为在许多体系结构上调用函数指针非常慢,乐观的例子
从内存中获取地址,3个周期以上。 在某些处理器19+周期等待ip值,10个周期的同时延迟流水线。
如果最复杂的现代cpu可以预测实际的跳转地址[BTB]以及它进行分支预测,那么这可能是一种损失。 否则,~8个额外指令将轻松保存由于流水线停滞而丢失的4 *(3 + 10)指令(如果预测失败率小于10-20%)。
如果两个if中的分支都被预测(即评估为假),则丢失的~2个周期很好地被内存延迟覆盖以获得呼叫地址,并且我们并没有更糟糕。
如果其中一个if是错误预测,那么BTB很可能也是错误的。 那么错误预测的成本大约是8个周期,其中3个由内存延迟支付,正确的不占用或第2个如果可以节省一天或我们支付完整的10+管道停顿。
如果仅存在两种可能性, 则将采用其中一种,并且我们将保存来自函数指针调用的管道停顿,并且我们将最大。 得到一个误预测导致没有(显着)比直接调用更差的性能。 如果内存延迟较长并且结果得到正确预测,则效果会更大。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.