std::thread调用虚成员函数受sleep影响?C++11该行为是否符合预期?
关于C++11中基类构造函数启动线程调用虚函数的行为分析
你观察到的这个输出结果其实是一种未定义行为的“巧合”,并不是C++11标准规定的预期行为,这种写法藏着不小的风险,咱们一步步拆解来看:
首先先还原你的测试代码和运行结果:
测试代码
#include <iostream> #include <thread> using namespace std; class A { public: virtual void a() = 0; thread t; A() : t(&A::a, this) {} virtual ~A() { t.join(); } }; class B : public A { public: virtual void a() { cout << "B::a" << endl; } }; int main() { B b; this_thread::sleep_for(chrono::seconds(1)); }
编译运行命令及结果
$ g++ -std=c++11 -pthread test.cpp -o test $ ./test B::a
为什么会出现这个结果?
按照C++的构造规则,创建B对象时会先执行基类A的构造函数,此时B的成员还没初始化,对象的动态类型是A——如果这时直接调用虚函数a(),因为A::a是纯虚函数,程序会直接崩溃。
但你的代码是在A的构造函数里启动了一个线程,线程要执行的是&A::a并传入this。这里的关键是线程的执行时机是操作系统调度决定的:在你的测试中,线程刚好等到B的构造函数完成后才开始执行a(),这时对象的动态类型已经变成了B,所以调用了B::a,才有了输出。
为什么这是危险的?
这种行为完全不可靠:如果操作系统在B的构造函数完成前就让线程执行a(),此时对象还处于基类构造阶段,调用纯虚函数A::a会直接导致程序崩溃;甚至在更复杂的场景下,可能出现对象状态不完整导致的诡异Bug,而且这类问题很难复现和调试。
正确的写法建议
不要在构造函数里启动线程调用虚函数,把线程启动延迟到派生类构造完成之后。比如给基类加一个启动方法:
#include <iostream> #include <thread> using namespace std; class A { public: virtual void a() = 0; thread t; A() {} virtual ~A() { if (t.joinable()) { t.join(); } } // 单独的启动方法 void start() { t = thread(&A::a, this); } }; class B : public A { public: virtual void a() { cout << "B::a" << endl; } }; int main() { B b; b.start(); // 派生类构造完成后再启动线程 this_thread::sleep_for(chrono::seconds(1)); }
这样就能保证线程启动时,对象已经完全构造完成,动态类型确定是B,调用a()的行为是完全符合预期且稳定的。
内容的提问来源于stack exchange,提问作者TechDragon
相关产品推荐
相关产品推荐

