我偶然发现了我认为是不知不觉射击自己脚部的一种非常简单的方法。
先介绍一下
数据成员的初始化顺序是数据成员的声明顺序。所以这是非法的:
<h1>My Title<p>My text</p></h1>
因为在struct A
{
std::size_t i_;
std::size_t length_;
A(std::size_t length)
: i_{length_} // UB here. `length_` is uninitialized
length_{length}
{}
};
的初始化程序中使用数据成员length_
时未初始化。幸运的是i_
和gcc
对此都给出了非常好的警告。一种简单的解决方案是根据参数clang
初始化每个数据成员。
现在到重点
但是当它不是立即显而易见时,该怎么办。例如。当数据成员是i_{length}
std::thread
使用数据成员初始化程序时也会出现相同的情况:
struct X
{
std::thread thread_;
std::mutex mutex_;
X() : thread_{&X::worker_thread, this}
{}
auto worker_thread() -> void
{
// use mutex_
std::lock_guard lk{mutex_}; // boom?
// ..
}
};
这看起来很无辜,struct X
{
std::thread thread_{&X::worker_thread, this};
std::mutex mutex_;
};
和gcc
均未警告这种情况。这并不奇怪,因为依赖项是隐藏的。
我想上面的情况并不罕见,因此我正在确认这确实是UB。最后声明clang
数据成员,或者默认将其初始化并在以后分配。
答案 0 :(得分:5)
是的,这的确是未定义的行为。实际上,您已经使示例与线程和互斥体过于复杂了。每次在初始化成员时(显式或隐式)使用 // LookAt = translation + direction
mLookAt[0] = headSetX + directionX;
mLookAt[1] = headSetY + directionY;
mLookAt[2] = headSetZ + directionZ;
时,您都会遇到麻烦。较简单的示例:
this
从成员初始化中调用非静态成员函数总是非常危险的;从构造函数主体调用成员函数时,通常也要小心。
答案 1 :(得分:1)
有两种可能的UB:
mutex_
的成员函数的调用是UB。mutex_
的初始化和对其的访问导致出现问题的数据争用。成员函数的调用将导致UB
15.7.1对于具有非平凡构造函数的对象,在构造函数开始执行之前引用该对象的任何非静态成员或基类都会导致不确定的行为。
33.3.2.2线程构造函数[...] 6.同步:构造函数的调用完成与f副本的调用开始同步。
33.4.3.2.3互斥锁类型应为DefaultConstructible和Destructible。
mutex_
的初始化在std :: thread初始化之后进行排序(因为它们是数据成员),该初始化与线程的开头同步。并且如果std::mutex
不可构造(这是未指定的)。然后,由于在构造对象之前访问对象,这将导致潜在的UB。鉴于成员函数的调用和初始化可能是并发的。
用于数据竞赛:
6.8.2.1如果两个表达式求值中的一个修改了内存位置(6.6.1),而另一个表达式读取或修改了相同的内存位置,则冲突。
6.8.2.1.20如果一个程序的执行包含两个潜在的并发冲突动作,其中至少一个不是原子动作,并且没有一个在另一个动作之前发生,则执行程序包含一个数据争用,除了信号处理程序的特殊情况外如下面所描述的。任何此类数据争用都会导致不确定的行为。
很有可能std::mutex
的构造将修改某些需要由std :: mutex :: lock修改的内存位置,但是也很有可能这种修改是原子的。但是它们没有被标准指定。
结论是,我认为这种用法是否会导致不确定的行为尚无定论。