clang 的 global-constructors 警告是否过于严格?

Is clang's global-constructors warning too strict?

在我们的项目中,我们经常使用这样的构造(为清楚起见进行了简化,我们实际上使用了一个更安全的使用版本):

struct Info
{
    Info(int x, int y) : m_x(x), m_y(y)
    {}

    int m_x;
    int m_y;
};

struct Data
{
    static const Info M_INFO_COLLECTION[3];
};

const Info Data::M_INFO_COLLECTION[] =  // global-constructors warning
{
    Info(1, 2),
    Info(10, 9),
    Info(0, 1)
};

M_INFO_COLLECTION 可以包含大量数据点。初始化部分驻留在cpp文件中,通常是代码生成的。

现在这个结构在我们的代码库中给了我们相当数量的 global-constructors-警告。我读过 in a blog post that using warnings in the -Weverything group is a bad idea for nightly builds and I agree, and even the clang docdoes not recommend to use it

由于我无法决定关闭警告,我可以使用 a helpful trick 来消除警告(以及潜在的 static 初始化顺序失败 ), 通过将静态成员转换为初始化并返回局部静态变量的函数。

但是,由于我们的项目没有按规定使用动态分配的内存,所以原来的想法不得不使用没有指针,这会导致deinitialization problems当我的Dataclass 被其他对象以一种奇怪的方式使用。

所以,长话短说:global-constructors 警告指向一段我可以检查为安全的代码,因为我知道 Data class 的作用。如果 other classes 以特定方式使用 Data,我可以使用可能导致问题的解决方法来摆脱它,但这不会生成警告。我的结论是,我最好保留代码原样并忽略警告。

所以现在我遇到了一堆警告,在某些情况下可能指向 SIOF 并且我想解决这些问题,但是这些警告被我故意不想修复的大量警告所掩盖,因为修复实际上会使事情变得更糟。

这让我想到了我的实际问题:clang 对警告的解释是否过于严格?根据我有限的编译器理解,编译器是否应该意识到在这种特殊情况下,静态成员 M_INFO_COLLECTION 不可能导致 SIOF,因为它的所有依赖项都是非静态的?

我试了一下这个问题,甚至这段代码也收到了警告:

//at global scope

int get1() 
{
    return 1;
}

int i = get1(); // global-constructors warning

虽然这工作正常,正如我所期望的那样:

constexpr int get1() 
{
    return 1;
}

int i = 1;  // no warning
int j = get1(); // no warning

这对我来说看起来很微不足道。我是不是遗漏了什么或者 clang 应该能够抑制这个例子的警告(也可能是我上面的原始例子)?

问题是它没有常量初始化。这意味着 M_INFO_COLLECTION 可能是 zero-initialized 然后在 运行 时动态初始化。

由于“全局构造函数”(non-constant 初始化),您的代码生成汇编以动态设置 M_INFO_COLLECTIONhttps://godbolt.org/z/45x6q6

这导致意外行为的示例:

// data.h
struct Info
{
    Info(int x, int y) : m_x(x), m_y(y)
    {}

    int m_x;
    int m_y;
};

struct Data
{
    static const Info M_INFO_COLLECTION[3];
};


// data.cpp
#include "data.h"

const Info Data::M_INFO_COLLECTION[] =
{
    Info(1, 2),
    Info(10, 9),
    Info(0, 1)
};


// main.cpp
#include "data.h"

const int first = Data::M_INFO_COLLECTION[0].m_x;

int main() {
    return first;
}

现在,如果您在 data.cpp 之前编译 main.cppfirst 可能会在其生命周期之外访问 Info。实际上,这个 UB 只是让 first 0.

例如,

$ clang++ -I. main.cpp data.cpp -o test
$ ./test ; echo $?
0
$ clang++ -I. data.cpp main.cpp -o test
$ ./test ; echo $?
1

当然,这是未定义的行为。在 -O1,这个问题消失了,并且 clang 的行为就好像 M_INFO_COLLECTION 是常量初始化的(as-if 它将动态初始化重新排序到 first 的动态初始化之前(以及所有其他动态初始化),这是允许的)。

解决这个问题的方法是不使用全局构造函数。如果你的static storage duration变量可以常量初始化,就把相关的 functions/constructors constexpr.

如果您无法添加 constexpr 来使用 non-constant 初始化变量,那么您可以解决静态初始化顺序失败的问题没有使用 placement-new:

的动态内存
// data.h
struct Info
{
    Info(int x, int y) : m_x(x), m_y(y)
    {}

    int m_x;
    int m_y;
};

struct Data
{
    static auto M_INFO_COLLECTION() -> const Info(&)[3];
    static const Info& M_ZERO();
};

// data.cpp
#include "data.h"

#include <new>

auto Data::M_INFO_COLLECTION() -> const Info(&)[3] {
    // Need proxy type for array reference
    struct holder {
        const Info value[3];
    };
    alignas(holder) static char storage[sizeof(holder)];
    static auto& data = (new (storage) holder{{
        Info(1, 2),
        Info(10, 9),
        Info(0, 1)
    }})->value;
    return data;
}

const Info& Data::M_ZERO() {
    // Much easier for non-array types
    alignas(Info) static char storage[sizeof(Info)];
    static const Info& result = *new (storage) Info(0, 0);
    return result;
}

尽管与常规静态存储持续时间变量相比,每次访问(尤其是第一次访问)的时间开销确实很小 运行。它应该比 new T(...) 技巧更快,因为它不调用内存分配运算符。


简而言之,最好添加 constexpr 以便能够不断初始化静态存储持续时间变量。