我正在编写关于编译器设计的任务。在代码生成部分,我遇到了如何创建指令以确保在运行时调用适当的方法。该语言是C ++的一个非常小的子集。
让我们说:
void main()
{
Animal* a;
a = new Cow;
//what code should be generated to ensure that object 'a' calls Cow::Init here
a->Init(5);
}
class Cow : public Animal{
void Init(int h)
{
height = h;
}
}
class Animal {
int height;
virtual void Init(int h){
height = h;
}
}
答案 0 :(得分:2)
这是一种非常简单的方法(请注意:这不包括在编译时对已知调用进行优化): 如果你的类有任何虚拟成员(包括继承),那么它的第一个成员就成了一个指向vftable的指针。 vftable是每个类定义的常量,这就是你只需要一个指针的原因。
从那里开始,每个唯一函数在该vftable中分配一个索引,因此每个唯一名称(注意:按名称我的意思是包含类型的符号名称,但没有类名称空间限定)具有唯一索引,那么表是从继承树顶部的类填充到当前工作类定义。这样,虚拟函数的更新重定义将覆盖共享其索引的旧条目。调用函数然后变得微不足道,因为你只是生成对该函数的名称索引的索引的调用。
因此,在您的示例中,Animal
有一个带有1个条目的{vvtable Init(int)
,它被赋予唯一索引0.因此您有一个类似于此的vftable:
;Animal - vftable
&Animal::Init //note: this isn't a class member pointer in the C++ sense, its a namespaced function pointer if you will
然后当您为Cow
构建vftable时,使用Animals
作为基础并添加虚函数,在本例中为Init(int)
,但它已经具有唯一索引0,所以我们覆盖索引0处的函数:
;Cow - vftable
&Cow::Init
然后如果我们有电话:
a->Init(5);
我们只是将其转换为:
a->vftable[0](5);
其中0是分配给Init(int)
的唯一索引。
一个汇编示例,以防万一:
;ecx contains our class pointer
mov eax,[ecx] ;get the vftable ptr
mov eax,[eax] ; get the ptr at (vftable + (unique_index * sizeof(func_ptr)))
push 5 ;push our arg 5, ecx is already setup for __thiscall
call eax ; let it rip!
注意:这一切都假定您的符号表设置为能够检测通过继承传递的虚函数或从继承变为虚函数的函数。
如果要对此进行优化,您可以分析a
,并发现它只分配了一次值,因此您可以将其类转换为所分配值的类Cow
。然后看到你在派生链的 end 上有一个类,你可以折叠vftable调用并直接调用Cow::Init
,这是多么棘手,以及有很多方法可以优化vftable调用,对于一个项目它应该没关系。
答案 1 :(得分:2)
这可以用轻量级C ++表示,如果你发现它比汇编更可读(我这样做)。我将自己限制在C(主要是)并且只是添加继承以避免大量的转换。
为清楚起见,实施细节将以__
为前缀。请注意,这些标识符通常保留给实现,因此通常不应在程序中使用它们。
一种类型安全的虚拟调度方法。
注意:仅限于简单继承(单基,无虚继承)
让我们创建Animal
类。
struct __AnimalTableT;
struct Animal { __AnimalTableT const * const __vptr; int height; }
void AnimalInit(Animal* a, int height) {
a->height = height;
}
我们在Animal中保留指向虚拟表的指针的空间,并将该方法表示为外部函数,以使this
显式化。
接下来,我们“创建”虚拟表。请注意,C中的数组需要由相似的元素组成,因此我们将使用稍高级别的方法。
struct __AnimalTableT {
typedef void (*InitFunction)(int);
InitFunction Init;
};
static __AnimalTableT const __AnimalTable = { &AnimalInit };
现在,让我们创建一个牛:
struct Cow: Animal {};
void CowInit(Animal* a, int height) {
Cow* c = static_cast<Cow*>(a);
c->height = height;
}
以及相关表格:
// Note: we could have new functions here (that only Cow has)
// they would be appended after the "Animal" part
struct __CowTableT: __AnimalTableT {};
static __CowTableT const __CowTable = { &CowInit };
用法:
typedef void (*__AnimalInitT)(Animal*,int);
int main() {
Cow cow = { &__CowTable, 0 };
__AnimalInitT const __ai = cow.__vptr->Init;
(*__ai)(&cow, 5);
}
真正的那个?
实际使用稍微复杂一些,但建立在相同的想法上。
正如您所指出的,CowInit
将Animal*
指针作为其第一个参数,这很奇怪。问题是您需要与原始重载方法的兼容函数指针类型。在线性继承的情况下它并不重要,但是在多继承或虚拟继承的情况下,事情变得相当繁忙,Animal
Cow
子部分可能不会被放置在开始,导致指针调整。
在现实生活中,我们有thunk:
好吧,我们可以将CowInit
的签名更改为更自然:
void CowInit(Cow* cow, int height);
然后,我们通过创建“thunk”来“弥合”差距,以进行改编:
void __CowInit(Animal* a, int height) {
CowInit(static_cast<Cow*>(a), height);
}
static __CowTableT const __CowTable = { &__CowInit };
在现实生活中,我们有表格:
另一个评论是,结构的使用非常好,但我们在这里谈论的是一个实现细节,因此不需要精确。通常,编译器因此使用普通数组,而不是结构:
typedef (void)(*__GenericFunction)();
static __GenericFunction const __AnimalTable[] = {
__GenericFunction(&AnimalInit)
};
static __GenericFunction const __CowTable[] = {
__GenericFunction(&__CowInit)
};
这稍微改变了调用:您使用索引而不是属性名称,并且需要转换回适当的函数类型。
typedef void (*__AnimalInitT)(Animal*,int);
int main() {
Cow cow = { &__CowTable, 0 };
// old line: __AnimalInitT const __ai = cow.__vptr->Init;
__AnimalInit const __ai = __AnimalInit(cow.__vptr[0]);
(*__ai)(&cow, 5);
}
如您所见,表的使用实际上是一个实现细节。
这里真正重要的一点是引入了一个 thunk 来调整功能签名。请注意,thunk是在创建 Derived 类(Cow here)的表时引入的。在我们的例子中,它是不必要的,因为在低级别,两个对象具有相同的地址,所以我们可以不用,并且智能编译器不会生成它并直接使用&CowInit
。
答案 2 :(得分:0)
该概念通常使用thunk实现,{{3}}是编译器生成的包装函数。