在我们的项目中,我们经常使用这样的构造(为了清楚起见,进行了简化,我们实际上使用了更安全的版本):
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在 -Weverything
组中使用警告对于夜间构建来说是一个坏主意,我同意,甚至 clang 文档 does not recommend to use it .
由于关闭警告的决定不在我手中,我可以使用 a helpful trick通过将静态成员转换为初始化并返回局部静态变量的函数来消除警告(以及潜在的静态初始化顺序失败)。
但是,由于我们的项目一般不使用动态分配的内存,所以最初的想法必须在没有指针的情况下使用,这可能会导致deinitialization problems当我的 Data
类被其他对象以奇怪的方式使用时。
所以,长话短说: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;
}
现在,如果您在 data.cpp
之前编译 main.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
或 have 来使用非常量初始化变量,那么您可以使用以下方法解决静态初始化顺序失败的问题,而无需使用动态内存展示位置-新
:
// 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/