全局变量 - 何时使用 static、inline、extern、const 和 constexpr

Global variables - When to use static, inline, extern, const, and constexpr

提问人:Jan Schultke 提问时间:8/29/2023 更新时间:8/31/2023 访问量:362

问:

有很多与 C++ 全局变量相关的问题和答案,例如:

它们都包含小片段的信息,例如变量在 C++17 中的具体工作方式等。其中一些也没有很好地老化,因为 C++17 通过引入变量从根本上改变了规则。inlineinline

C++ 还引入了模块和模块链接,再次使 StackOverflow 上的大多数内容过时,甚至过时。

本问答试图将这些问题统一起来,并为读者提供考虑这些版本更改的概述。

问题

  • 何时对全局变量使用 、 、 、 等?staticinlineexternconstconstexpr
  • 答案在历史上是如何变化的(C++17之前/之后,C++20之前/之后)?
  • 答案如何根据全局变量的位置(标头/源/模块)而变化?
C 常量 全局变量 链接 C++-FAQ

评论

2赞 Pepijn Kramer 8/29/2023
首先不要使用全局变量(所以根本不需要 extern),对于头文件中的全局常量,它应该是 ,对于 cpp 文件中的常量。 用于不可变参数(以及不更改对象状态的成员函数)。如果您有全局状态,请使用依赖关系注入。inline constexpr <type>{value}constexpr <type>{value};const
0赞 Pepijn Kramer 8/29/2023
轻微更正:用于 cpp 文件中的常量static constexpr <type>{value};
0赞 Jan Schultke 8/29/2023
@PepijnKramer我认为人们被告知不要过多地使用全局变量,如果你确实有一个有效的用例,那么问题仍然是如何实现它。/ 也没有涵盖几乎所有内容,因为 C++98、C++11、C++17 和 C++20(带模块)的答案会有所不同。inlinestaticconstexpr
0赞 Pepijn Kramer 8/29/2023
公平地说,我的答案主要是关于 17/20
0赞 chrisroode 8/29/2023
我认为值得一提的是,你编程的背景与如何回答这些问题有很大关系。例如,我目前正在为用 C 语言编写的飞行控制软件进行代码审查。它们有一个编码标准,即任何作用域的函数文件都是静态的。如果您正在为 Windows 构建,这可能没有必要,但在他们的环境中,这是必需的。最好深入了解这些术语,以便您可以在特定用例中应用最佳实践。

答:

5赞 8 revsJan Schultke #1

何时对全局变量使用 、 、 、 等?staticinlineexternconstconstexpr

0. 概述

全局变量用例 常数 非常量
单个源文件的
本地文件(即声明且仅在
单个文件中使用,未在标头中声明)
static const
(C++11)
匿名命名空间(C++11)
static constexprconst
static
匿名命名空间(C++11)
已声明,未在标头中定义,
在源文件中定义
extern const在标题中;
在源代码中,或者
(C++11)在源代码中
constconstexpr
extern在标题中;
源代码中是普通的
在标头中定义,
直到 C++17
模仿模板
和/或 (C++11);或
仅适用于整数
inlineconstconstexprenum
使用模板进行模仿inline
在头文件中定义,
从 C++17
inline const
inline constexpr
inline
单个模块的本地(C++20) const或;
选择
constexprinline
选择inline
由模块导出(C++20) export const
export inline constexpr
export;
选择inline

在上述所有情况下,(从 C++20 开始)也可以使用,但不能与 结合使用。 也有用,和不一样。constinitconstexprconstinit constconstexpr

注意:该决定也可能根据全局变量是否在动态链接库中定义/使用以及其他因素而改变。

1.始终使用确保一个源文件本地的所有内容都具有内部链接

首先,如果全局变量是在单个源文件中声明的,并且只在单个源文件中使用,那么它必须具有内部链接。 在以下情况下,全局变量具有内部链接:

  • 它被标记为static
  • 它位于匿名命名空间中(从 C++11 开始)
  • 它是(或(自 C++11 以来),因为暗示constconstexprconstexprconst)

如果它没有内部链接,那么你很容易遇到ODR违规。以下示例格式不正确,无需诊断。

// a.cpp
int counter = 0;   // FIXME: surround with anonymous namespace, or add 'static'
// b.cpp
long counter = 0;  // FIXME: surround with anonymous namespace, or add 'static'

创建变量(从 C++17 开始)并不能解决此问题。 内部链接使其安全,因为每个 TU 中的两个 s 是不同的。inlinecounter

2. 如果声明了某些内容,但未在标头中定义,请将其设为extern

有时,每个人都有一个定义并不重要。例如:

// log.hpp
extern std::ofstream log_file;
// log.cpp
std::ofstream log_file = open_log_file();

将 to 的定义放在标题中是没有意义的,因此在任何地方都可见。在性能方面可以获得很少的收益,这将迫使我们也在任何地方都可见。log_fileopen_log_file()

注意(自 C++11 起)或(自 C++20 起)extern constexprextern const constinit

另一个有效但罕见的用例是(自 C++11 以来)(请参阅此答案),即 在标头中,(自 C++11 起)在源中。 如果可用,则通过源代码中的(从 C++20 开始)更好地表达这一点。extern constexprextern constconstexprconst constinit

此模式的目的是避免对初始化成本高昂的查找表进行动态初始化,同时保持标头/源拆分。

3. 如果在标头中定义了某些内容,请制作它(从 C++17 开始),或使用模板进行模仿(直到 C++17) inlineinline

有时,在任何地方都有一个定义是很重要的,例如对于应该内联的全局常量:

inline constexpr float exponent = 1.25f;
问:我真的需要与?inlineconstexpr

是的,你有。请看以下示例:

constexpr float exponent = 1.25f; // OK so far, but dangerous

// note: 'const float&' might seem contrived because you could work with 'float'.
//       However, templates use const& everywhere, so it's very easy to
//       run into this case indirectly.
inline const float& foo(const float& x) {
    // IFNDR if the definition of foo appears in multiple translation units (TUs).
    return std::max(x, exponent);
}

该程序可能格式不正确,不需要诊断,因为具有内部链接(由于存在),并且是每个 TU 中的一个不同对象。 每个定义都可能返回对自己唯一值的不同引用,这违反了 [basic.def.odr] p14.5exponentconstfooexponent

问:在 C++17 之前我可以做什么? 变量尚不存在。inline

类似的机制一直以模板的形式存在。

// define a wrapper class template with a static data member
template <typename = void>
struct helper { static const float exponent; };
// define the static data member
template <typename T>
struct helper<T>::exponent = 1.25f;
// For convenience, make a reference with internal linkage to it.
// This is safe from ODR violations because
// [basic.def.odr] p14.5.2 has a special case for it, and
// this special case was retroactively applied to all C++ standards
// in the form of a defect report.
static constexpr float& exponent = helper<>::exponent;
// (static const float& prior to C++11)

或者,特别是对于作用域(自 C++11 起)和无作用域枚举,可以将定义放在标头中而不会有风险:

inline constexpr int array_size = 100; // since C++17
enum { array_size = 100; };            // pre-C++17 alternative

4. 尽可能制作所有全局变量甚至(从 C++11 开始)constconstexpr

除了 1.、2.3 中的规则之外,总是尽可能地做事。 这极大地简化了确保程序正确性的过程,并实现了额外的编译器优化。const

问:如果初始化很复杂,我该怎么办?

有时你不能只用一个简单的表达式来初始化一个全局变量,就像下面这样:

std::array<float, 100> lookup;

void init() {
    for (std::size_t i = 0; i < lookup.size(); ++i) {
        lookup[i] = compute(i);
    }
}

但是,不要延迟初始化;而是使用立即调用的 lambda 表达式 (IILE)(自 C++11 起)或常规函数来执行初始化。

constexpr std::array<float, 100> lookup = [] {
    decltype(lookup) result{};
    /* ... */
    return result;
}();
问:OR(自 C++11 以来)对链接有什么影响?constconstexpr

制作一个全局变量 or(从 C++11 开始)给了它内部链接,但正如 3 中解释的那样,这并不能为您简化任何事情。 您仍然需要担心链接和 ODR。constconstexpr

5. 静态数据成员的工作方式不同

3 所示,静态数据成员可能不遵循相同的规则。 它们与它们所属的类具有相同的联系,其后果如下:

  • 在类模板的情况下,静态数据成员是准的。inline
  • const并不意味着静态数据成员的内部链接。

此外,对于静态数据成员意味着(自 C++17 起)。constexprinline