You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

vtable创建/填充时机探究:为何虚函数调用结果不一致?

构造期间虚函数调用与vtable初始化问题

问题现象

运行以下简化代码时,会随机出现两种不同的输出结果:一种是回调调用子类的methodToCall,另一种是调用父类的methodToCall,差异的核心在于线程执行时机与vtable初始化的竞争。

简化代码

#include <iostream>
#include <functional>
#include <thread>

using namespace std;

typedef std::function<void (void)> MyCallback;

MyCallback clb = NULL;

void callCallbackFromThread(void){
    while(clb == NULL);
    clb();
}

int someLongTask(void){
    volatile unsigned int i = 0;
    cout << "Start someLongTask" << endl;
    while(i < 100000000)
        i++;
    cout << "End someLongTask" << endl;
    return i;
}

class Base{
public:
    Base(){
        clb = std::bind(&Base::_methodToCall, this);
        cout << "Base-Constructor" << endl;
    }
protected:
    virtual void methodToCall(void){
        cout << "Base methodToCall got called!" << endl;
    };
private:
    void _methodToCall(void){
        methodToCall();
    }
};

class Child: public Base{
public:
    Child()
     : dummy(someLongTask()){
        cout << "Child Constructor" << endl;
    }
protected:
    void methodToCall(void){
        cout << "Child methodToCall got called!" << endl;
    }
private:
    int dummy;
};

int main()
{
    thread t(callCallbackFromThread);
    Child child;
    t.join();
    clb();

    return 0;
}

两种输出结果

结果1

Base-Constructor
Start someLongTask
Child methodToCall got called!
End someLongTask
Child Constructor
Child methodToCall got called!

结果2

Base-ConstructorBase methodToCall got called!
Start someLongTask

End someLongTask
Child Constructor
Child methodToCall got called!

原因解析

C++中,类的vtable初始化严格遵循构造顺序:

  1. 构造Child对象时,首先执行父类Base的构造函数。此时对象的vtable会被设置为Base的vtable——因为在父类构造期间,对象还不是完整的Child对象,编译器会确保此时调用虚函数只会解析到父类版本。
  2. 父类构造完成后,才会执行子类的成员初始化(此处是调用someLongTask)和子类构造函数体。在子类构造逻辑启动前,编译器会将对象的vtable替换为Child的vtable。

两种结果对应两种线程竞争场景:

  • 场景1:线程在someLongTask执行期间调用回调——此时父类构造已完成,子类vtable也已替换完毕,因此methodToCall调用子类版本。
  • 场景2:线程在父类构造完成后、子类vtable替换前调用回调——此时对象vtable仍为Base的,因此methodToCall调用父类版本。

另外,全局变量clb的访问未加同步机制,存在数据竞争,属于C++未定义行为,这是两种结果随机出现的直接诱因。

修复建议

  • 避免在构造函数中注册回调或启动线程:构造期间对象尚未完全初始化,极易引发此类问题。
  • 添加同步机制:如果必须在构造中处理多线程逻辑,需用互斥量等手段确保对象完全初始化后才允许回调执行。
  • 构造完成后再注册回调:比如给Child类添加init方法,在对象构造完成后手动调用以注册回调,而非在父类构造中完成。

内容的提问来源于stack exchange,提问作者Muperman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 13:17:00