【问题标题】:How does the compiler generate code for virtual function calls?编译器如何为虚函数调用生成代码?
【发布时间】:2014-02-04 20:48:28
【问题描述】:

CAT *p;
...
p->speak();
...

有些书说编译器会将 p->speak() 翻译成:

(*p->vptr[i])(p); //i is the idx of speak in the vtbl

我的问题是:因为在编译时,不可能知道 p 的真实类型, 这意味着不可能知道要使用哪个 vptr 或 vtbl。那么,编译器如何生成正确的代码呢?

[修改]

例如:

void foo(CAT* c)
{
    c->speak();
    //if c point to SmallCat
    // should translate to (*c->vptr[i])(p); //use vtbl at 0x1234   
    //if c point to CAT
    // should translate to (*c->vptr[i])(p); //use vtbl at 0x5678  

    //since ps,pc all are CAT*, why does compiler can generate different code for them 
    //in compiler time?
}

...
CAT *ps,*pc;
ps = new SmallCat;  //suppose SmallCat's vtbl address is 0x1234;
pc = new CAT;       //suppose CAT's vtbl address is 0x5678;
...
foo(ps);
foo(pc)
...

有什么想法吗?谢谢。

【问题讨论】:

  • 最后它是编译器实现特定的......
  • @camino 现在您的新箭头使图表看起来正确!

标签: c++


【解决方案1】:

您的图片缺少的是从CATSmallCAT 对象到它们对应的vtbls 的箭头。编译器将指向 vtbl 的指针嵌入到对象本身中——可以将其视为隐藏的成员变量。这就是为什么说添加第一个虚函数“花费”每个对象在内存占用中一个指针的原因。指向 vtbl 的指针是由构造函数中的代码设置的,因此为了在运行时访问其 vtable,编译器生成的所有虚拟调用需要做的是取消引用指向 this 的指针。

当然,虚拟继承和多重继承会变得更加复杂:编译器需要生成稍微不同的代码,但基本过程保持不变。

以下是您的示例的详细说明:

CAT *p1,*p2;
p1 = new SmallCat;  //suppose its vtbl address is 0x1234;
// The layout of SmallCat object includes a vptr as a hidden member.
// At this point, the value of this vptr is set to 0x1234.
p2 = new CAT;       //suppose its vtbl address is 0x5678;
// The layout of Cat object also includes a vptr as a hidden member.
// At this point, the value of this vptr is set to 0x5678.
(*p1->vptr[i])(p); //should use vtbl at 0x1234
// Compiler has enough information to do that, because it squirreled away 0x1234
// inside the SmallCat object at the time it was constructed.
(*p2->vptr[i])(p); //should use vtbl at 0x5678
// Same deal - the constructor saved 0x5678 inside the Cat, so we're good.

【讨论】:

  • 如果我们不知道p的类型,我们怎么知道我们应该使用CAT的vtbl还是SmallCAT的vtbl。因为在编译的时候我们没有真正的对象,所以我们不能用它的vptr来获取对应的vtbl吧?
  • 我已经添加了一个例子,希望它能让我的问题清楚
  • @camino 指向 vtbl 的指针嵌入在对象中,这些对象始终位于同一位置。当您将Cat 传递给foo 时,c->vptr 的计算结果为0x5678;当您通过 SmallCat 时,c->vptr 的计算结果为 0x1234speak的调用解引用它,抓取第一个位置的指针,调用vtbl[i]内部指针指向的函数。
  • @camino 正确,编译器确保它们是正确的。
  • @camino:事实上,vptr 几乎总是第一个成员
【解决方案2】:

这意味着不可能知道要使用哪个 vptr 或 vtbl

在方法调用期间是正确的。但是在构造的时候,构造的对象的类型其实是已知的,编译器会在ctor中生成代码来初始化vptr指向对应类的vtbl。后面所有的虚方法调用都会通过这个vptr调用右边vtbl中的方法。

有关此初始化如何与基础对象(按顺序调用多个 ctor)一起工作的更多详细信息,请参阅this answer 的类似问题。

【讨论】:

  • 这也是为什么你不应该在构造函数中调用虚函数的原因。
  • @Alex:在 C++ 中没有理由回避它:这种行为是安全的(与 Java 不同,在 Java 中你可能会触及未初始化的成员,因此不鼓励这样做)。不鼓励使用 C++ 的人通常会混淆 2 种语言,这是那些人的问题,而不是语言。
  • @MSalters:人们在 C++ 中不鼓励它不是因为它不安全,而是因为它不直观,因此容易出错。
  • @MSalters 我想说如果人们不知道 C++ 如何构建它的对象,他们会感到困惑。从理论上讲,这也可能是我正在构建派生对象的另一种方式,一旦我设置了 vptr,我将转到基类并初始化它们。一般来说,这种行为可能会令人惊讶。如果您知道编译器在做什么,那么这样做是完全有效的。这不是要引起未定义的行为,而是要让其他可能阅读它的人感到困惑。
  • @Alex:无论如何,您都应该知道构造顺序,这不仅是因为虚函数,还因为所有成员都是按照从基到派生的顺序创建的。 C++ 竭尽全力帮助您:this-> 只允许访问已构建的成员。
【解决方案3】:

编译器隐式地向每个具有一个或多个虚函数的类添加一个名为 vptr 的指针。

您可以通过在此类上使用 sizeof 来判断这一点,并看到它比您预期的大 4 或 8 个字节,具体取决于 sizeof(void*)

编译器还会在每个类的构造函数中添加一段隐式代码,它将vptr 设置为指向函数指针表(也称为 V 表)。

当一个对象被实例化时,它的类型被明确地“提及”。

例如:A a(1)A* p = new B(2)

所以在构造函数内部,在运行时vptr 可以很容易地设置为指向正确的 V-Table。

在上面的例子中:

  • avptr 设置为指向class A 的V-Table。

  • pvptr 设置为指向 class B 的 V-Table。

顺便说一句,构造函数与所有其他函数不同,事实上你必须显式使用对象类型才能调用它(因此构造函数永远不能被声明为虚拟的)。

下面是编译器为虚函数p->speak()生成正确代码的方式:

CAT *p;
...
p = new SuperCat("SaberTooth",2); // p->vptr = SuperCat_Vtable
...
p->speak(); // See pseudo assembly code below

Ax = p               // Get the address of the instance
Bx = p->vptr         // Get the address of the instance's V-Table
Cx = Bx + CAT::speak // Add the number of the function in its class
Dx = *Cx             // Get the address of the appropriate function
Push Ax              // Push the address of the instance into the stack
Push Dx              // Push the address of the function into the stack
CallF                // Save some registers and jump to the beginning of the function

编译器对class CAT 层次结构中的所有speak 函数使用相同的编号(索引)。

下面是编译器为非虚函数p->eat()生成正确代码的方式:

p->eat(); // See pseudo assembly code below

Ax = p        // Get the address of the instance
Bx = CAT::eat // Get the address of the function
Push Ax       // Push the address of the instance into the stack
Push Bx       // Push the address of the function into the stack
CallF         // Save some registers and jump to the beginning of the function

由于eat函数的地址在编译时是已知的,因此汇编代码效率更高。

最后,'vptr' 在运行时如何设置为指向正确的 V-Table:

class SmallCat
{
    void* vptr; // implicitly added by the compiler
    ...         // your explicit variables
    SmallCat()
    {
        vptr = (void*)0x1234; // implicitly added by the compiler
        ...                   // Your explicit code
    }
};

当您实例化CAT* p = new SmallCat() 时,会创建一个新对象,其vptr = 0x1234

【讨论】:

  • 在编译时我们没有真正的对象,所以我们不能用它的vptr来获取对应的vtbl吧?
  • 不,但正如我所说,我们可以添加代码,在运行时将设置 vptr 指向正确的 V-Table。
  • 我已经添加了一个例子,希望它能让我的问题清楚
  • 为您的示例添加了“伪编译”;请查看修改后的答案。
  • 当然可以! c 指向CAT 类型的对象或SmallCat 类型的对象。创建该对象时,其vptr 字段被设置为指向正确的VTable。请查看更新的答案。
【解决方案4】:

当你写这个时(我已经用小写替换了所有用户代码):

class cat {
public:
    virtual void speak() {std::cout << "meow\n";}
    virtual void eat() {std::cout << "eat\n";}
    virtual void destructor() {std::cout << "destructor\n";}
};

编译器会神奇地生成所有这些(我所有的示例编译器代码都是大写的):

class cat;
struct CAT_VTABLE_TYPE { //here's the cat's vtable type
    void(*speak)(cat* this); //contains a pointer for each virtual function
    void(*eat)(cat* this);
    void(*destructor)(cat* this);
};
extern CAT_VTABLE_TYPE CAT_VTABLE; //later is a global shared copy of the vtable
class cat { //here's the class you typed
private:
    CAT_VTABLE_TYPE* vptr; //but the compiler adds this magic member
public:
    cat() :vptr(&CAT_VTABLE) {} //the compiler initializes the vtable ptr
    ~cat() {vptr->destructor(this);} //redirects to the one you coded
    void speak() {vptr->speak(this);} //redirects to the one you coded
    void eat() {vptr->eat(this);} //redirects to the one you coded
};

//Here's the functions you programmed
void DEFAULT_CAT_SPEAK(CAT* this) {std::cout << "meow\n";}
void DEFAULT_CAT_EAT(CAT* this) {std::cout << "eat\n";}
void DEFAULT_CAT_DESTRUCTOR(CAT* this) {std::cout << "destructor\n";}
//and the global cat vtable (shared by all cat objects)
const CAT_VTABLE_TYPE CAT_VTABLE = {
    DEFAULT_CAT_SPEAK, 
    DEFAULT_CAT_EAT, 
    DEFAULT_CAT_DESTRUCTOR};

嗯,很多不是吗? (实际上我有点作弊,因为我在定义对象之前获取了对象的地址,但这种方式代码更少,混淆更少,即使在技术上不可编译)你可以看到他们为什么将它构建到语言中。而且...这是 SmallCat 之前的:

class smallcat : public cat {
public:
    virtual void speak() {std::cout << "meow2\n";}
    virtual void destructor() {std::cout << "destructor2\n";}
};

之后:

class smallcat;
//here's the smallcat's vtable type
struct SMALLCAT_VTABLE_TYPE : public CAT_VTABLE_TYPE { 
     //contains no additional virtual functions that cat didn't have
};
extern SMALLCAT_VTABLE_TYPE SMALLCAT_VTABLE; //later is a global shared copy of the vtable
class smallcat : public cat { //here's the class you typed
public:
    smallcat() :vptr(&SMALLCAT_VTABLE) {} //the compiler initializes the vtable ptr
    //The other functions already are virtual, nothing additional needed
};
//Here's the functions you programmed
void DEFAULT_SMALLCAT_SPEAK(CAT* this) {std::cout << "meow2\n";}
void DEFAULT_SMALLCAT_DESTRUCTOR(CAT* this) {std::cout << "destructor2\n";}
//and the global cat vtable (shared by all cat objects)
const SMALLCAT_VTABLE_TYPE SMALLCAT_VTABLE = {
    DEFAULT_SMALLCAT_SPEAK, 
    DEFAULT_CAT_EAT, //note: eat wasn't overridden
    DEFAULT_SMALLCAT_DESTRUCTOR};

因此,如果阅读内容过多,编译器会为每个 type 创建一个 VTABLE 对象,该对象指向该特定类型的成员函数,然后将指向该 VTABLE 的指针插入其中每个实例。

当您创建smallcat 对象时,编译器会构造cat 父对象,它将vptr 分配为指向CAT_VTABLE 全局。紧接着,编译器构造了smallcat 派生对象,它覆盖vptr 成员,使其指向SMALLCAT_VTABLE 全局。

当您调用c-&gt;speak(); 时,编译器会生成调用它是cat::speak 的副本(看起来像this-&gt;vptr-&gt;speak(this);)。 vptr 成员可能指向全局 CAT_VTABLE 或全局 SMALLCAT_VTABLE,因此该表的 speak 指针指向 DEFAULT_CAT_SPEAK(您在 cat::speak 中输入的内容)或 DEFAULT_SMALLCAT_SPEAK (您在smallcat::speak 中放置的代码)。所以this-&gt;vptr-&gt;speak(this); 最终会调用派生度最高的类型的函数,无论派生度最高的类型是什么。

总而言之,这无疑是非常令人困惑的,因为编译器会在编译时神奇地重命名函数。实际上,由于多重继承,实际上它比我在这里展示的要混乱得多。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-05-29
    • 1970-01-01
    • 1970-01-01
    • 2013-05-22
    • 1970-01-01
    相关资源
    最近更新 更多