Closed
Description
#include <iostream>
#include <thread>
struct X{
X(){
// 假设 X 的初始化没那么快
std::this_thread::sleep_for(std::chrono::seconds(1));
std::puts("X");
}
};
struct Test{
Test(): t{&Test::f, this} // 线程已经开始执行
{}
~Test(){
if(t.joinable())
t.join();
}
void f()const{ // 如果在函数执行的线程 f 中使用 x 则会存在问题。使用了未初始化的数据成员 ub
std::cout<<"f\n";
}
std::thread t; // 声明顺序决定了初始化顺序,优先初始化 t
X x;
};
int main(){
Test t;
}
这本质还是初始化顺序问题,在第四章的 AudioPlayer
实现中就有此问题,需要注意。
std::atomic<bool> stop; // 控制线程的停止与退出,
std::thread player_thread; // 后台执行音频任务的专用线程
std::mutex mtx; // 保护共享资源
std::condition_variable cond; // 控制线程等待和唤醒,当有新任务时通知音频线程
std::queue<std::string> audio_queue; // 音频任务队列,存储待播放的音频文件路径
sf::Music music; // SFML 音频播放器,用于加载和播放音频文件
这样的声明顺序可能导致线程对象已经初始化,启动线程执行成员函数 playMusic
,而互斥量、条件变量、队列、音乐播放器 等成员还未初始化,产生未定义行为。
可以自行调整顺序顺序或直接将启动线程这件事情在构造函数体中做(因为这是最后的过程),又或者创建一个 start 函数,等等... 常见做法。
见文档:
初始化顺序
列表中的成员初始化器的顺序无关紧要:初始化的实际顺序如下:
- 如果构造函数是最终派生类的,那么按基类声明的深度优先、从左到右的遍历中的出现顺序(从左到右指的>是基说明符列表中所呈现的顺序),初始化各个虚基类。
- 然后,以在此类的基类说明符列表中出现的从左到右顺序,初始化各个直接基类。
- 然后,以类定义中的声明顺序,初始化各个非静态成员。
- 最后,执行构造函数体。