编译器如何知道 vtable 中的哪个条目对应于虚函数?
How does the compiler know which entry in vtable corresponds to a virtual function?
假设我们在父 class 和派生 class 中有多个虚函数。将为父派生的 class.
的 vtable 中的这些虚函数创建一个 vtable
编译器如何知道 vtable 中的哪个条目对应哪个虚函数?
示例:
class Animal{
public:
void fakeMethod1(){}
virtual void getWeight(){}
void fakeMethod2(){}
virtual void getHeight(){}
virtual void getType(){}
};
class Tiger:public Animal{
public:
void fakeMethod3(){}
virtual void getWeight(){}
void fakeMethod4(){}
virtual void getHeight(){}
virtual void getType(){}
};
main(){
Animal a* = new Tiger();
a->getHeight(); // A will now point to the base address of vtable Tiger
//How will the compiler know which entry in the vtable corresponds to the function getHeight()?
}
我在研究中没有找到确切的解释 -
=
"This table is used to resolve the function call as it contains the
addresses of all the virtual functions of that class."
table究竟是如何用来解析函数调用的?
=
"So at runtime, the code just uses the object's vptr to locate the
vtbl, and from there the address of the actual overridden function."
我无法理解这一点。 Vtable 保存的是虚函数的地址,而不是实际重写函数的地址。
它可以帮助您自己实现类似的东西。
struct Bob;
struct Bob_vtable {
void(*print)(Bob const*self) = 0;
Bob_vtable(void(*p)(Bob const*)):print(p){}
};
template<class T>
Bob_vtable const* make_bob_vtable(void(*print)(Bob const*)) {
static Bob_vtable const table(+print);
return &table;
}
struct Bob {
Bob_vtable const* vtable;
void print() const {
vtable->print(this);
}
Bob():vtable( make_bob_vtable<Bob>([](Bob const*self){
std::cout << "Bob\n";
})) {}
protected:
Bob(Bob_vtable const* t):vtable(t){}
};
struct Alice:Bob {
int x = 0;
Alice():Bob( make_bob_vtable<Alice>([](Bob const*self){
std::cout << "Alice " << static_cast<Alice const*>(self)->x << '\n';
})) {}
};
这里我们有一个明确的 vtable 存储在 Bob
中。它指向一个 table 函数。非虚拟成员函数 print
使用它动态分派到正确的方法。
Bob
和派生 class Alice
的构造函数将 vtable 设置为具有不同值的不同值(在本例中创建为静态局部变量)在 table.
要使用的指针已包含在 Bob::print
含义的定义中——它知道 table.
中的偏移量
如果我们在Alice中再增加一个虚函数,实际上就是vtable指针会指向一个struct Alice_vtable:Bob_vtable
。 Static/reinterpret 强制转换将使我们得到 "real" table,我们可以轻松访问额外的函数指针。
当我们谈论虚继承 以及虚函数时,事情就变得奇怪了。我没有资格描述它是如何工作的。
我将稍微修改您的示例,以便它显示面向对象的更多有趣方面。
假设我们有以下内容:
#include <iostream>
struct Animal
{
int age;
Animal(int a) : age {a} {}
virtual int setAge(int);
virtual void sayHello() const;
};
int
Animal::setAge(int a)
{
int prev = this->age;
this->age = a;
return prev;
}
void
Animal::sayHello() const
{
std::cout << "Hello, I'm an " << this->age << " year old animal.\n";
}
struct Tiger : Animal
{
int stripes;
Tiger(int a, int s) : Animal {a}, stripes {s} {}
virtual void sayHello() const override;
virtual void doTigerishThing();
};
void
Tiger::sayHello() const
{
std::cout << "Hello, I'm a " << this->age << " year old tiger with "
<< this->stripes << " stripes.\n";
}
void
Tiger::doTigerishThing()
{
this->stripes += 1;
}
int
main()
{
Tiger * tp = new Tiger {7, 42};
Animal * ap = tp;
tp->sayHello(); // call overridden function via derived pointer
tp->doTigerishThing(); // call child function via derived pointer
tp->setAge(8); // call parent function via derived pointer
ap->sayHello(); // call overridden function via base pointer
}
我忽略了一个好的建议,即 class 带有 virtual
函数成员的元素应该有一个 virtual
析构函数用于此示例。无论如何我都要泄露这个对象。
让我们看看如何将此示例转换为良好的旧 C,其中没有成员函数,更不用说 virtual
了。以下所有代码都是 C,不是 C++。
struct animal
很简单:
struct animal
{
const void * vptr;
int age;
};
除了 age
成员之外,我们还添加了一个 vptr
,它将成为指向 vtable 的指针。我为此使用了一个 void
指针,因为无论如何我们都必须进行丑陋的转换,使用 void *
可以稍微减少丑陋。
接下来,我们可以实现成员函数了。
static int
animal_set_age(void * p, int a)
{
struct animal * this = (struct animal *) p;
int prev = this->age;
this->age = a;
return prev;
}
注意附加的第 0 个参数:在 C++ 中隐式传递的 this
指针。同样,我使用了 void *
指针,因为它会在以后简化事情。请注意 inside 任何成员函数,我们总是 知道 静态 this
指针的类型,因此转换没有问题。 (而且在机器级别,它根本不做任何事情。)
sayHello
成员的定义类似,只是这次 this
指针是 const
合格的。
static void
animal_say_hello(const void * p)
{
const struct animal * this = (const struct animal *) p;
printf("Hello, I'm an %d year old animal.\n", this->age);
}
动物 vtable 的时间到了。首先我们必须给它一个类型,它是直截了当的。
struct animal_vtable_type
{
int (*setAge)(void *, int);
void (*sayHello)(const void *);
};
然后我们创建 vtable 的单个实例并使用正确的成员函数对其进行设置。如果 Animal
有一个纯 virtual
成员,相应的条目将有一个 NULL
值并且最好不要取消引用。
static const struct animal_vtable_type animal_vtable = {
.setAge = animal_set_age,
.sayHello = animal_say_hello,
};
请注意 animal_set_age
和 animal_say_hello
已声明为 static
。这是可行的,因为它们永远不会通过名称引用,而只能通过 vtable 引用(并且 vtable 只能通过 vptr
所以它也可以是 static
)。
我们现在可以实现 Animal
…
的构造函数
void
animal_ctor(void * p, int age)
{
struct animal * this = (struct animal *) p;
this->vptr = &animal_vtable;
this->age = age;
}
…和对应的operator new
:
void *
animal_new(int age)
{
void * p = malloc(sizeof(struct animal));
if (p != NULL)
animal_ctor(p, age);
return p;
}
唯一有趣的是在构造函数中设置 vptr
的行。
让我们继续讨论老虎。
Tiger
继承自 Animal
所以它得到一个 struct tiger
子对象。我通过将 struct animal
作为第一个成员来做到这一点。这是第一个成员很重要,因为这意味着该对象的第一个成员——vptr
——与我们的对象具有相同的地址。我们稍后会在进行一些棘手的转换时需要它。
struct tiger
{
struct animal base;
int stripes;
};
我们也可以简单地在 struct tiger
定义的开头按词法复制 struct animal
的成员,但这可能更难维护。编译器不关心这种风格问题。
我们已经知道如何实现老虎的成员函数了
void
tiger_say_hello(const void * p)
{
const struct tiger * this = (const struct tiger *) p;
printf("Hello, I'm an %d year old tiger with %d stripes.\n",
this->base.age, this->stripes);
}
void
tiger_do_tigerish_thing(void * p)
{
struct tiger * this = (struct tiger *) p;
this->stripes += 1;
}
请注意,我们这次将 this
指针转换为 struct tiger
。如果调用了老虎函数,this
指针最好指向老虎,即使我们是通过基指针调用的。
虚表旁边:
struct tiger_vtable_type
{
int (*setAge)(void *, int);
void (*sayHello)(const void *);
void (*doTigerishThing)(void *);
};
请注意,前两个成员与 animal_vtable_type
完全相同。这是必不可少的,基本上是您问题的直接答案。如果我将 struct animal_vtable_type
作为第一个成员,也许会更明确。我想强调的是,对象布局本来 完全相同 除了我们不能在这种情况下玩我们讨厌的投射技巧。同样,这些是 C 语言的方面,不存在于机器级别,因此编译器不会为此烦恼。
创建一个 vtable 实例:
static const struct tiger_vtable_type tiger_vtable = {
.setAge = animal_set_age,
.sayHello = tiger_say_hello,
.doTigerishThing = tiger_do_tigerish_thing,
};
并实现构造函数:
void
tiger_ctor(void * p, int age, int stripes)
{
struct tiger * this = (struct tiger *) p;
animal_ctor(this, age);
this->base.vptr = &tiger_vtable;
this->stripes = stripes;
}
老虎构造函数做的第一件事是调用动物构造函数。还记得动物构造函数如何将 vptr
设置为 &animal_vtable
吗?这就是为什么从基础 class 构造函数调用 virtual
成员函数时常让人感到惊讶的原因。只有基础class构造函数有了运行后,我们才重新将vptr
赋值给派生类型,然后自己初始化
operator new
只是样板文件。
void *
tiger_new(int age, int stripes)
{
void * p = malloc(sizeof(struct tiger));
if (p != NULL)
tiger_ctor(p, age, stripes);
return p;
}
我们完成了。但是我们如何调用虚成员函数呢?为此,我将定义一个辅助宏。
#define INVOKE_VIRTUAL_ARGS(STYPE, THIS, FUNC, ...) \
(*((const struct STYPE ## _vtable_type * *) (THIS)))->FUNC( THIS, __VA_ARGS__ )
现在,这很丑陋。它所做的是采用静态类型 STYPE
、this
指针 THIS
和成员函数的名称 FUNC
以及传递给函数的任何其他参数。
然后,它从静态类型构造vtable的类型名。 (##
是预处理器的标记粘贴运算符。例如,如果 STYPE
是 animal
,那么 STYPE ## _vtable_type
将扩展为 animal_vtable_type
。)
接下来,THIS
指针被强制转换为指向刚刚派生的 vtable 类型的指针。这是有效的,因为我们确保将 vptr
作为每个对象中的 first 成员,因此它具有相同的地址。这是必不可少的。
完成后,我们可以解引用指针(以获取实际的 vptr
),然后请求它的 FUNC
成员并最终调用它。 (__VA_ARGS__
扩展为额外的可变宏参数。)请注意,我们还将 THIS
指针作为第 0 个参数传递给成员函数。
现在,实际情况是我不得不为不带参数的函数再次定义几乎相同的宏,因为预处理器不允许可变参数宏参数包为空。就这样吧。
#define INVOKE_VIRTUAL(STYPE, THIS, FUNC) \
(*((const struct STYPE ## _vtable_type * *) (THIS)))->FUNC( THIS )
有效:
#include <stdio.h>
#include <stdlib.h>
/* Insert all the code from above here... */
int
main()
{
struct tiger * tp = tiger_new(7, 42);
struct animal * ap = (struct animal *) tp;
INVOKE_VIRTUAL(tiger, tp, sayHello);
INVOKE_VIRTUAL(tiger, tp, doTigerishThing);
INVOKE_VIRTUAL_ARGS(tiger, tp, setAge, 8);
INVOKE_VIRTUAL(animal, ap, sayHello);
return 0;
}
您可能想知道
中发生了什么
INVOKE_VIRTUAL_ARGS(tiger, tp, setAge, 8);
来电。我们正在做的是在通过 struct tiger
指针引用的 Tiger
对象上调用 Animal
的非覆盖 setAge
成员。该指针首先被隐式转换为 void
指针,并作为 this
指针传递给 animal_set_age
。然后该函数将其转换为 struct animal
指针。这个对吗?是的,因为我们小心地将 struct animal
作为 struct tiger
中的第一个成员,所以 struct tiger
对象的地址与 struct animal
对象的地址相同子对象。这与我们玩 vptr
.
的技巧相同(只少一级)
假设我们在父 class 和派生 class 中有多个虚函数。将为父派生的 class.
的 vtable 中的这些虚函数创建一个 vtable编译器如何知道 vtable 中的哪个条目对应哪个虚函数?
示例:
class Animal{
public:
void fakeMethod1(){}
virtual void getWeight(){}
void fakeMethod2(){}
virtual void getHeight(){}
virtual void getType(){}
};
class Tiger:public Animal{
public:
void fakeMethod3(){}
virtual void getWeight(){}
void fakeMethod4(){}
virtual void getHeight(){}
virtual void getType(){}
};
main(){
Animal a* = new Tiger();
a->getHeight(); // A will now point to the base address of vtable Tiger
//How will the compiler know which entry in the vtable corresponds to the function getHeight()?
}
我在研究中没有找到确切的解释 -
=
"This table is used to resolve the function call as it contains the addresses of all the virtual functions of that class."
table究竟是如何用来解析函数调用的?
=
"So at runtime, the code just uses the object's vptr to locate the vtbl, and from there the address of the actual overridden function."
我无法理解这一点。 Vtable 保存的是虚函数的地址,而不是实际重写函数的地址。
它可以帮助您自己实现类似的东西。
struct Bob;
struct Bob_vtable {
void(*print)(Bob const*self) = 0;
Bob_vtable(void(*p)(Bob const*)):print(p){}
};
template<class T>
Bob_vtable const* make_bob_vtable(void(*print)(Bob const*)) {
static Bob_vtable const table(+print);
return &table;
}
struct Bob {
Bob_vtable const* vtable;
void print() const {
vtable->print(this);
}
Bob():vtable( make_bob_vtable<Bob>([](Bob const*self){
std::cout << "Bob\n";
})) {}
protected:
Bob(Bob_vtable const* t):vtable(t){}
};
struct Alice:Bob {
int x = 0;
Alice():Bob( make_bob_vtable<Alice>([](Bob const*self){
std::cout << "Alice " << static_cast<Alice const*>(self)->x << '\n';
})) {}
};
这里我们有一个明确的 vtable 存储在 Bob
中。它指向一个 table 函数。非虚拟成员函数 print
使用它动态分派到正确的方法。
Bob
和派生 class Alice
的构造函数将 vtable 设置为具有不同值的不同值(在本例中创建为静态局部变量)在 table.
要使用的指针已包含在 Bob::print
含义的定义中——它知道 table.
如果我们在Alice中再增加一个虚函数,实际上就是vtable指针会指向一个struct Alice_vtable:Bob_vtable
。 Static/reinterpret 强制转换将使我们得到 "real" table,我们可以轻松访问额外的函数指针。
当我们谈论虚继承 以及虚函数时,事情就变得奇怪了。我没有资格描述它是如何工作的。
我将稍微修改您的示例,以便它显示面向对象的更多有趣方面。
假设我们有以下内容:
#include <iostream>
struct Animal
{
int age;
Animal(int a) : age {a} {}
virtual int setAge(int);
virtual void sayHello() const;
};
int
Animal::setAge(int a)
{
int prev = this->age;
this->age = a;
return prev;
}
void
Animal::sayHello() const
{
std::cout << "Hello, I'm an " << this->age << " year old animal.\n";
}
struct Tiger : Animal
{
int stripes;
Tiger(int a, int s) : Animal {a}, stripes {s} {}
virtual void sayHello() const override;
virtual void doTigerishThing();
};
void
Tiger::sayHello() const
{
std::cout << "Hello, I'm a " << this->age << " year old tiger with "
<< this->stripes << " stripes.\n";
}
void
Tiger::doTigerishThing()
{
this->stripes += 1;
}
int
main()
{
Tiger * tp = new Tiger {7, 42};
Animal * ap = tp;
tp->sayHello(); // call overridden function via derived pointer
tp->doTigerishThing(); // call child function via derived pointer
tp->setAge(8); // call parent function via derived pointer
ap->sayHello(); // call overridden function via base pointer
}
我忽略了一个好的建议,即 class 带有 virtual
函数成员的元素应该有一个 virtual
析构函数用于此示例。无论如何我都要泄露这个对象。
让我们看看如何将此示例转换为良好的旧 C,其中没有成员函数,更不用说 virtual
了。以下所有代码都是 C,不是 C++。
struct animal
很简单:
struct animal
{
const void * vptr;
int age;
};
除了 age
成员之外,我们还添加了一个 vptr
,它将成为指向 vtable 的指针。我为此使用了一个 void
指针,因为无论如何我们都必须进行丑陋的转换,使用 void *
可以稍微减少丑陋。
接下来,我们可以实现成员函数了。
static int
animal_set_age(void * p, int a)
{
struct animal * this = (struct animal *) p;
int prev = this->age;
this->age = a;
return prev;
}
注意附加的第 0 个参数:在 C++ 中隐式传递的 this
指针。同样,我使用了 void *
指针,因为它会在以后简化事情。请注意 inside 任何成员函数,我们总是 知道 静态 this
指针的类型,因此转换没有问题。 (而且在机器级别,它根本不做任何事情。)
sayHello
成员的定义类似,只是这次 this
指针是 const
合格的。
static void
animal_say_hello(const void * p)
{
const struct animal * this = (const struct animal *) p;
printf("Hello, I'm an %d year old animal.\n", this->age);
}
动物 vtable 的时间到了。首先我们必须给它一个类型,它是直截了当的。
struct animal_vtable_type
{
int (*setAge)(void *, int);
void (*sayHello)(const void *);
};
然后我们创建 vtable 的单个实例并使用正确的成员函数对其进行设置。如果 Animal
有一个纯 virtual
成员,相应的条目将有一个 NULL
值并且最好不要取消引用。
static const struct animal_vtable_type animal_vtable = {
.setAge = animal_set_age,
.sayHello = animal_say_hello,
};
请注意 animal_set_age
和 animal_say_hello
已声明为 static
。这是可行的,因为它们永远不会通过名称引用,而只能通过 vtable 引用(并且 vtable 只能通过 vptr
所以它也可以是 static
)。
我们现在可以实现 Animal
…
void
animal_ctor(void * p, int age)
{
struct animal * this = (struct animal *) p;
this->vptr = &animal_vtable;
this->age = age;
}
…和对应的operator new
:
void *
animal_new(int age)
{
void * p = malloc(sizeof(struct animal));
if (p != NULL)
animal_ctor(p, age);
return p;
}
唯一有趣的是在构造函数中设置 vptr
的行。
让我们继续讨论老虎。
Tiger
继承自 Animal
所以它得到一个 struct tiger
子对象。我通过将 struct animal
作为第一个成员来做到这一点。这是第一个成员很重要,因为这意味着该对象的第一个成员——vptr
——与我们的对象具有相同的地址。我们稍后会在进行一些棘手的转换时需要它。
struct tiger
{
struct animal base;
int stripes;
};
我们也可以简单地在 struct tiger
定义的开头按词法复制 struct animal
的成员,但这可能更难维护。编译器不关心这种风格问题。
我们已经知道如何实现老虎的成员函数了
void
tiger_say_hello(const void * p)
{
const struct tiger * this = (const struct tiger *) p;
printf("Hello, I'm an %d year old tiger with %d stripes.\n",
this->base.age, this->stripes);
}
void
tiger_do_tigerish_thing(void * p)
{
struct tiger * this = (struct tiger *) p;
this->stripes += 1;
}
请注意,我们这次将 this
指针转换为 struct tiger
。如果调用了老虎函数,this
指针最好指向老虎,即使我们是通过基指针调用的。
虚表旁边:
struct tiger_vtable_type
{
int (*setAge)(void *, int);
void (*sayHello)(const void *);
void (*doTigerishThing)(void *);
};
请注意,前两个成员与 animal_vtable_type
完全相同。这是必不可少的,基本上是您问题的直接答案。如果我将 struct animal_vtable_type
作为第一个成员,也许会更明确。我想强调的是,对象布局本来 完全相同 除了我们不能在这种情况下玩我们讨厌的投射技巧。同样,这些是 C 语言的方面,不存在于机器级别,因此编译器不会为此烦恼。
创建一个 vtable 实例:
static const struct tiger_vtable_type tiger_vtable = {
.setAge = animal_set_age,
.sayHello = tiger_say_hello,
.doTigerishThing = tiger_do_tigerish_thing,
};
并实现构造函数:
void
tiger_ctor(void * p, int age, int stripes)
{
struct tiger * this = (struct tiger *) p;
animal_ctor(this, age);
this->base.vptr = &tiger_vtable;
this->stripes = stripes;
}
老虎构造函数做的第一件事是调用动物构造函数。还记得动物构造函数如何将 vptr
设置为 &animal_vtable
吗?这就是为什么从基础 class 构造函数调用 virtual
成员函数时常让人感到惊讶的原因。只有基础class构造函数有了运行后,我们才重新将vptr
赋值给派生类型,然后自己初始化
operator new
只是样板文件。
void *
tiger_new(int age, int stripes)
{
void * p = malloc(sizeof(struct tiger));
if (p != NULL)
tiger_ctor(p, age, stripes);
return p;
}
我们完成了。但是我们如何调用虚成员函数呢?为此,我将定义一个辅助宏。
#define INVOKE_VIRTUAL_ARGS(STYPE, THIS, FUNC, ...) \
(*((const struct STYPE ## _vtable_type * *) (THIS)))->FUNC( THIS, __VA_ARGS__ )
现在,这很丑陋。它所做的是采用静态类型 STYPE
、this
指针 THIS
和成员函数的名称 FUNC
以及传递给函数的任何其他参数。
然后,它从静态类型构造vtable的类型名。 (##
是预处理器的标记粘贴运算符。例如,如果 STYPE
是 animal
,那么 STYPE ## _vtable_type
将扩展为 animal_vtable_type
。)
接下来,THIS
指针被强制转换为指向刚刚派生的 vtable 类型的指针。这是有效的,因为我们确保将 vptr
作为每个对象中的 first 成员,因此它具有相同的地址。这是必不可少的。
完成后,我们可以解引用指针(以获取实际的 vptr
),然后请求它的 FUNC
成员并最终调用它。 (__VA_ARGS__
扩展为额外的可变宏参数。)请注意,我们还将 THIS
指针作为第 0 个参数传递给成员函数。
现在,实际情况是我不得不为不带参数的函数再次定义几乎相同的宏,因为预处理器不允许可变参数宏参数包为空。就这样吧。
#define INVOKE_VIRTUAL(STYPE, THIS, FUNC) \
(*((const struct STYPE ## _vtable_type * *) (THIS)))->FUNC( THIS )
有效:
#include <stdio.h>
#include <stdlib.h>
/* Insert all the code from above here... */
int
main()
{
struct tiger * tp = tiger_new(7, 42);
struct animal * ap = (struct animal *) tp;
INVOKE_VIRTUAL(tiger, tp, sayHello);
INVOKE_VIRTUAL(tiger, tp, doTigerishThing);
INVOKE_VIRTUAL_ARGS(tiger, tp, setAge, 8);
INVOKE_VIRTUAL(animal, ap, sayHello);
return 0;
}
您可能想知道
中发生了什么INVOKE_VIRTUAL_ARGS(tiger, tp, setAge, 8);
来电。我们正在做的是在通过 struct tiger
指针引用的 Tiger
对象上调用 Animal
的非覆盖 setAge
成员。该指针首先被隐式转换为 void
指针,并作为 this
指针传递给 animal_set_age
。然后该函数将其转换为 struct animal
指针。这个对吗?是的,因为我们小心地将 struct animal
作为 struct tiger
中的第一个成员,所以 struct tiger
对象的地址与 struct animal
对象的地址相同子对象。这与我们玩 vptr
.