4

我偶然发现了一种我认为非常简单的方法,可以在不知不觉中击中自己的脚。


先简单介绍一下

数据成员的初始化顺序是数据成员的声明顺序。所以这是非法的:

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_. 幸运的是,gccclang为此给出了非常好的警告。简单的解决方案是从参数初始化每个数据成员,即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警告clang这种情况。这并不奇怪,因为依赖是隐藏的。

我会想象上述情况并不少见,所以我正在确认这确实是 UB。最后声明std::mutex数据成员,或者默认初始化并稍后分配。

4

2 回答 2

6

是的,这确实是未定义的行为。事实上,您使用线程和互斥锁使示例过于复杂。每次你this在成员的初始化中使用(显式或隐式),你都会给自己带来麻烦。更简单的例子:

struct A {
    int y;
    int x = 0;
    A() : y(sety()) { }

    int sety() { return x; } // Ka-boom!
};

从成员初始化中调用非静态成员函数总是很危险的;从构造函数体调用成员函数时,通常也必须小心。

于 2018-11-15T16:10:11.473 回答
1

有两种可能的 UB:

  • 如果这里的成员函数调用的mutex_是UB。
  • 如果初始化mutex_和访问它导致有问题的数据竞争。

成员函数的调用会导致 UB 如果,

  1. std::thread 的构造在 std::mutex 构造之前排序,并且
  2. std::mutex 不是简单可构造的。

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 修改的内存位置,但这种修改也很有可能是原子的。但标准没有规定它们。

作为结论,我认为这种用法是否会导致未定义的行为是未指定的。

于 2018-11-15T16:15:58.887 回答