【问题标题】:Is clang's global-constructors warning too strict?clang 的 global-constructors 警告是否过于严格?
【发布时间】:2020-10-25 23:09:20
【问题描述】:

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

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 docdoes not recommend to use it

由于我无法决定关闭警告,我可以使用a helpful trick 来消除警告(以及潜在的静态初始化命令失败),方法是将静态成员到一个初始化并返回一个局部静态变量的函数中。

但是,由于我们的项目通常不使用动态分配的内存,所以原来的想法必须在没有指针的情况下使用,这会导致deinitialization problems当我的Data类被其他对象使用时很奇怪方式。

所以,长话短说:global-constructors 警告指向一段我可以安全审查的代码,因为我知道Data 类的作用。如果 other 类以特定方式使用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

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

【问题讨论】:

  • 在源代码本身中根据需要禁用警告#pragma clang diagnostic ignored "-Wglobal-constructors"。对于我的项目,我在项目中禁用了“嘈杂的蟋蟀”警告,但如果这不是一个选项,您可以在代码中根据具体情况进行操作。
  • @cigien 我已经发现了这个问题,它让我想到了使用带有局部静态变量的函数的想法。在这个问题中,我想解决为什么 clang 无法处理明显没有问题的情况。我同意这两个问题密切相关,但存在细微差别。
  • @Eljay 这是个好主意。我会检查我是否可以通过我们的部门来解决这个问题,尽管我更愿意在全球范围内关闭。
  • 好的,听起来不错。我会留下评论,这样再加上你的回复应该确保它不会被关闭。这些标题至少使它们看起来非常相似:)

标签: c++ clang compiler-warnings


【解决方案1】:

问题是它不是常量初始化的。这意味着M_INFO_COLLECTION 可能会被零初始化,然后在运行时动态初始化。

由于“全局构造函数”(非常量初始化),您的代码会生成 assembley 以动态设置 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 是常量初始化(好像它把动态初始化重新排序到first 的动态初始化(以及所有其他动态初始化)之前),这是允许的做)。

对此的解决方法是不使用全局构造函数。如果您的静态存储持续时间变量能够被常量初始化,请将相关函数/构造函数设为constexpr

如果您无法添加constexprs 或拥有 以使用非常量初始化变量,那么您可以使用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 以便能够不断初始化您的静态存储持续时间变量。

【讨论】:

  • 您提出了一些非常好的观点,谢谢。我错过了另一个全局静态对象可能访问Data 的部分,因为我错误地认为这个调用会被编译器捕获(它没有),所以我现在更清楚地看到了警告的原因。即使我们通常不设置从其他静态对象访问我们的静态对象,这种情况也可能发生,我认为在这里保持安全是个好主意。
  • 您对constexpr 的建议听起来很令人兴奋,我了解到constexpr 的用法。我不知道如果我创建了类的构造函数constexpr(并且可能满足其他一些要求),我可以创建一个对象constexpr。我会检查我是否可以使我们的代码工作。乍一看,这听起来很有希望。谢谢。
  • 我想到了使用新展示位置的想法,但我认为它不会解决 SIOF,因为它不是动态的,所以我没有尝试,我可能不会已经找到了单独存储数据字节的解决方案。我不得不承认,我不完全理解为什么您的解决方案没有遇到我链接到的去初始化问题,也不明白为什么 isocpp faq 没有提到它。但如果它确实有效,这是一个很酷的解决方案,即使它有点笨拙。
  • @Cerno Placement new 和常规 new T() 都具有动态存储持续时间。我们不调用delete~T(),所以销毁后使用没有问题,因为它永远不会被销毁
  • @Cerno 如果您有static holder storage;,您将调用holder 的析构函数,包括程序末尾的成员数组(可能在其他析构函数使用它之前),运行到整个无序再次静力学问题。字节数组是放置 new 的默认值,因为它们没有析构函数和sizeof(char) == 1
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-03-12
  • 2015-03-15
  • 1970-01-01
  • 2017-04-26
  • 2011-09-19
  • 1970-01-01
  • 2012-08-27
相关资源
最近更新 更多