在我们的项目中,我们经常使用这样的构造(为简化起见,我们实际上使用了更安全的版本):
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.cpp
,first
可能会在其生命周期之外访问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/