【问题标题】:Defining global constants in C++1z?在 C++1z 中定义全局常量?
【发布时间】:2017-08-16 10:06:34
【问题描述】:

C++1z 中声明内存高效全局常量 的最佳方法是什么?internal linkage ,所以在所有 translation units 中使用单个副本?

虽然在很多地方都提到过,但我们没有任何单一的“最佳方法”问题和答案,所以在这里。以下是我发现相关问题的部分地点列表。

我正在寻找至少适用于所有基本基本类型的解决方案,例如 intdoublecharstd::string

编辑 1: 最好的方式,我的意思是“内存效率”。我知道对于内部链接,每个翻译单元都会制作多个副本,因此会占用内存。除此之外,如果我们能够实现快速的执行时间和编译时间,那就太好了,美化(易于)代码并不重要。我也知道外部链接会增加获取时间,因为处理器可能会在运行时出现缓存未命中。但是最新的 C++ 语言是否支持所有这些的最佳方法?或者可能会为每个案例提供建议支持?

注意:另外,std::string 全局常量的最佳方法是什么? mutability 会不会对 constness 有影响?

【问题讨论】:

  • 只是don't use globals。
  • @HenriMenke 但是有一些常量是应用方面的,比如 PI 的值,或者一些需要在任何地方调用的标准错误消息。
  • 标准库似乎更喜欢inline constexpr
  • @HenriMenke 你在第一个 cmets 中提到的是关于全局 变量,这个问题是关于全局 constants

标签: c++ constants c++17


【解决方案1】:

总结

在 C++17 中,定义全局常量或全局变量的最简单方法通常是使用 inline variable。您只需在头文件中添加如下一行即可创建一个:

inline const std::string greeting = "Hello!";

如果全局常量是literal type,最好使用inline constexprinline 是可选的,如果它是静态类成员)而不是inline const

inline constexpr std::string_view greeting = "Hello!"sv;

这也适用于变量,但许多优点不再适用,因此您可能需要使用其他方法:

inline unsigned int score = 0;

详情

首先,此方法的两个主要缺点优点是:

  1. 它在 C++17 之前不起作用,因此如果您需要 C++14 或更早版本的兼容性,则需要执行其他操作。 template trick VTT suggests 与旧标准兼容,并且应该具有与内联变量类似的语义,但如果您只需要 C++17 支持,则不再需要它。
  2. 编译比extern variables 慢一些,因为编译器需要合并多个定义,并且优化器可以获得更多信息。如果您可以更改定义而不是数据类型,则“有点”部分会变成“明显”;由于该值现在位于头文件中,这意味着重新编译包含头文件的所有内容,而不仅仅是重新链接。如果您可能需要更改数据类型,无论如何都需要重新编译。

如果这两个对您来说都不重要,我认为这种方法胜过其他使用external linkage 获取全局常量的方法,并且在最终可执行文件中最多¹一个定义²。

像这样的内联变量只在一个文件中提及,因此很容易更改;这对于仅头文件的库特别有用。这也意味着它在编译时具有可用的价值,因此优化器可以看到它并可能消除一些用法。

使用constexpr

在 C++17 中,静态 constexpr 类成员自动成为 inline,所以如果你的全局常量应该是类作用域的一部分,你可以这样做

constexpr int favorite_number = -3;

否则,您需要说constexpr inline,它应该仍然有效。这将具有上述语义,同时也具有constexpr 的通常优点,因此编译器会知道它可以在编译时尝试做更多事情。例如:

#include <string_view>

using namespace std::literals;

inline constexpr std::string_view greeting = "Hello!"sv;

inline constexpr int scrabble_points[greeting.size()] = {4, 1, 1, 1, 1, 0};

int main() {
  int total = 0;
  for (int i : scrabble_points) {
    total += i;
  }
  return total;
}

可以用constexpr,但不能只用inline,因为constexpr它知道greeting.size()是一个编译时常数,可以用作数组的大小。³通过优化,这个could compile to a just a single mov instruction and ret,不包括字符串或数组的任何个副本,因为它是不必要的。

使用新的内联语义,main 之前的所有内容都可以包含在多个位置的头文件中,并且最多仍然存在一个副本。

变量

同样的方法很容易通过省略const来支持可变变量:

inline std::string player_name = "<subject name here>";

这是一个具有外部链接的全局变量。因为它是一个变量,所以我提到的 Pete’s answer 的大部分优点都没有了,但一些(比如只在一个地方声明变量,不需要额外链接任何东西)仍然存在。不过,它们可能不值得稍微额外的编译时间和缺乏 C++14 兼容性。


¹ 对于 constconstexpr 变量,如果不需要,编译器/优化器可能会完全消除该变量。从理论上讲,它可能决定将其复制到一个立即值或其他东西;实际上,您可能不应该担心这一点,因为它只有在有充分理由的情况下才会这样做,这应该会使最终的可执行文件更小和/或更快。您可以使用 -Os 而不是 -O3 来调整它。

² 每个使用该常量的目标文件 仍然会有一个副本,但它们会在链接时合并。避免这种情况的唯一方法是使用 extern 变量。

³ 即使没有inline,或者数组只有const 而不是constexpr,这个简化的示例也可以工作,但这些对于更复杂的实际情况很有用。

【讨论】:

  • "constexpr 变量自动内联" 不,它们不是。只有constepxr 是静态数据成员。
  • @T.C.我现在更新了示例,而不仅仅是周围的文本。
【解决方案2】:

过去我们会在头文件中声明一个常量并在源文件中定义它:

// constants.h
extern const int size;

// constants.cpp
#include "constants.h"
const int size = 3;

// usage
std::cout << size << '\n';

但也许这太简单了;为什么不用 10 行模板和时髦的实例化语法来完善它?

【讨论】:

  • 但是先生,正如您已经知道的那样,const 使 internal linksODR 相悖,我认为。
  • @SahibYar -- const 默认有内部链接。将其标记为extern 会为其提供外部链接。我编写的代码没有 ODR 问题:size 只在一个地方定义。
  • 现在当我说size * 5 时,编译器将插入一个乘法指令,而不仅仅是一个立即数 15。太棒了!
  • @DanielH -- 是的,这是因为问题中要求常量没有内部链接,因此“在所有翻译单元中使用单个副本”。
  • @DanielH 这个问题现在是开放的,所以如果你想给出你的答案......
【解决方案3】:

我建议使用模板包装器,它允许在头文件中定义任何类型的全局常量,并且只会在所有翻译单元中生成一个副本(不需要 C++1z):

template<typename TDummy = void> class
t_GlobalValueHolder final
{
     public: using
     t_Value = const int; // can be anything

     public: static t_Value s_value;
};

template<typename TDummy>
typename t_GlobalValueHolder<TDummy>::t_Value
t_GlobalValueHolder<TDummy>::s_value{42};

// usage
::std::cout << t_GlobalValueHolder<>::s_value;

注意:通常对s_value 的访问应仅限于类/方法的子集,方法是使值受保护并从t_GlobalValueHolder 派生用户类,或者仅将所有相关项目列为t_GlobalValueHolder 的朋友。除了消除对包含值定义的翻译单元的需要之外,适当的访问控制是这种方法相对于“extern const”的另一个好处。

【讨论】:

  • 静态模板成员变量...我们不是每个实例化的情况都有一个新实例吗?
  • @Swift 只有一个实例。但是,如果这是一个常规(不是模板)类,它就不会像这样工作。
  • 啊,我明白了,只要 TDummy 是一样的。 c++17 是否允许在内联初始化的情况下避免外部声明?
  • @Swift 好吧,TDummy 可以通过将static_assert(::std::is_same&lt;void, TDummy&gt;::value, ""); 放在t_GlobalValueHolder 定义中来锁定为默认值(即void)。然而,通常不需要这样的检查,因为不太可能有人无意中用不同的参数实例化 t_GlobalValueHolder。这种方法甚至适用于 C++11 之前的标准(经过一些调整),它只是对模板静态变量的一种特殊处理。
  • 这非常冗长;在其他地方不断引用可能会有所帮助。
猜你喜欢
  • 1970-01-01
  • 2016-05-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多