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

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 -warnings。我读过in a blog post,在-Weverything组中使用警告对于夜间构建是个坏主意,我同意,甚至是clang doc does not recommend to use it
由于关闭警告的决定是我无法控制的,因此我可以使用a helpful trick消除警告(以及潜在的静态初始化顺序惨败),方法是将静态成员转换为初始化并返回局部静态变量的函数。
但是,由于我们的项目通常不使用动态分配的内存,因此必须在没有指针的情况下使用原始想法,当我的Data类被其他对象以怪异的方式使用时,这可能导致deinitialization problems
因此,长话短说:global-constructors警告指出了一段我可以安全阅读的代码,因为我知道Data类的作用。我可以使用一种变通办法摆脱它,如果其他类以特定方式使用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可以被零初始化,然后在运行时动态初始化。
由于“全局构造函数”(非恒定初始化),您的代码会生成汇编来动态设置M_INFO_COLLECTION:https://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;
}
现在,如果您在main.cpp之前编译data.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是常量初始化的(就像它将动态初始化重新排序到first的动态初始化(以及所有其他动态初始化)之前一样,允许这样做)。
解决此问题的方法是不使用全局构造函数。如果您的静态存储持续时间变量能够被常量初始化,则将相关的函数/构造函数设为constexpr
如果您无法添加constexpr或必须使用非常量初始化变量,则可以使用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以便能够不断初始化静态存储持续时间变量。

关于c++ - clang的全局构造函数警告是否太严格?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/64529733/

10-16 04:26