vtable 何时创建/填充?

Mup*_*man 0 c++ inheritance multithreading vtable

我很难弄清楚为什么我的代码不起作用。我将问题追溯到 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;
}
Run Code Online (Sandbox Code Playgroud)

当我运行这个时,我有时会得到结果:

Base-Constructor
Start someLongTask
Child methodToCall got called!
End someLongTask
Child Constructor
Child methodToCall got called!
Run Code Online (Sandbox Code Playgroud)

有时是这样的:

Base-ConstructorBase methodToCall got called!
Start someLongTask

End someLongTask
Child Constructor
Child methodToCall got called!
Run Code Online (Sandbox Code Playgroud)

有人可以向我解释为什么在一种情况下调用基本方法而在另一种情况下调用子方法吗?它肯定与线程执行的确切时间有关。对我来说,当线程调用回调时,vtable 似乎没有正确设置,但我不明白为什么。

use*_*522 6

vtable是C++语言的实现细节。如果您遵循语言规则并且不深入研究未定义的行为领域来在特定的语言实现中进行修改,那么您永远不必关心它。

您的代码失败是因为您没有使用多线程程序所需的任何适当的同步。

clb不是一个原子变量,因此,如果不使用一些同步原语(例如保护读写访问的互斥体),则不允许您在一个线程中存储它并在另一个线程中加载它。

在多个线程中访问非原子对象而不与其中至少一个线程同步进行读取称为数据争用,并且是C++ 中无条件未定义的行为。


正如 @Mat 所指出的,调用clb将使用的类对象的生命周期存在另一个竞争。构造期间的虚函数调用只允许直接或间接地从构造函数的路径进行。这不适用于线程中的调用,并且也没有同步来确保类对象的所有构造函数在clb调用之前已完成。因此又是未定义的行为。(这通俗地称为 vtable 指针上的数据竞争,因为这就是它在实践中失败的原因。)

  • 最重要的是, clb() 调用本身可能会在子对象的生命周期开始之前访问子对象(构造函数未完成),这也是 UB 的问题(并且在 vtable 指针本身上也存在竞争,假设使用它的实现) (3认同)