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当我的Data
class 被其他对象以一种奇怪的方式使用。
所以,长话短说: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_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
是常量初始化的(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
以便能够不断初始化静态存储持续时间变量。
在我们的项目中,我们经常使用这样的构造(为清楚起见进行了简化,我们实际上使用了一个更安全的使用版本):
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当我的Data
class 被其他对象以一种奇怪的方式使用。
所以,长话短说: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_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
是常量初始化的(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
以便能够不断初始化静态存储持续时间变量。