C ++中的全局常量11

时间:2014-05-14 12:56:27

标签: c++ c++11 const constexpr

在C ++中声明和定义全局常量的最佳方法是什么?我对C ++ 11标准最感兴趣,因为它在这方面做了很多修复。

[EDIT(澄清)]:在这个问题中,“全局常量”表示在任何范围内编译时已知的常量变量或函数。必须可以从多个翻译单元访问全局常量。它不一定是constexpr风格常量 - 可以是const std::map<int, std::string> m = { { 1, "U" }, { 5, "V" } };const std::map<int, std::string> * mAddr() { return & m; }。在这个问题中,我没有触及优选的好的范围或常数名称。让我们把这些问题留给另一个问题。 [END_EDIT]

我想知道所有不同情况的答案,所以让我们假设T是以下之一:

typedef    int                     T;  // 1
typedef    long double             T;  // 2
typedef    std::array<char, 1>     T;  // 3
typedef    std::array<long, 1000>  T;  // 4
typedef    std::string             T;  // 5
typedef    QString                 T;  // 6
class      T {
   // unspecified amount of code
};                                     // 7
// Something special
// not mentioned above?                // 8

我相信没有大的语义(我在这里不讨论好的命名或范围样式)3种可能的范围之间的区别:

// header.hpp
extern const T tv;
T tf();                  // Global
namespace Nm {
    extern const T tv;
    T tf();              // Namespace
}
struct Cl {
    static const T tv;
    static T tf();       // Class
};

但如果从下面的替代方案中选择更好的方法取决于上述声明范围之间的差异,请指出。

还要考虑在常量定义中使用函数调用的情况,例如: <some value>==f();。如何在常量初始化中调用函数会影响在备选方案之间进行选择?

  1. 让我们先考虑Tconstexpr构造函数。 明显的替代方案是:

    // header.hpp
    namespace Ns {
    constexpr T A = <some value>;
    constexpr T B() { return <some value>; }
    inline const T & C() { static constexpr T t = <some value>; return t; }
    const T & D();
    }
    
    // source.cpp
    const T & Ns::D() { static constexpr T t = <some value>; return t; }
    

    我认为AB最适合小T(例如,拥有多个实例或在运行时复制它不是问题),例如1-3,有时7。如果C很大,DT会更好,例如4,有时7

  2. T没有constexpr构造函数。备选方案:

    // header.hpp
    namespace Ns {
    extern const T a;
    inline T b() { return <some value>; }
    inline const T & c() { static const T t = <some value>; return t; }
    const T & d();
    }
    
    // source.cpp
    extern const T Ns::a = <some value>;
    const T & Ns::d() { static const T t = <some value>; return t; }
    

    由于静态初始化顺序失败,我通常不会使用a。据我所知,bcd非常安全,甚至是自C ++ 11以来的线程安全。 b似乎不是一个好的选择,除非T有一个非常便宜的构造函数,这对于非constexpr构造函数来说并不常见。我可以说c超过d的一个优点 - 没有函数调用(运行时性能); d优于c的一个优点 - 当常量值更改时,重新编译较少(这些优点也适用于CD)。我相信我错过了很多推理。请在答案中提供其他注意事项。

  3. 如果你想修改/测试上面的代码,你可以使用我的测试文件(只有header.hpp,source.cpp和上面代码片段的可编译版本,main.cpp从header.hpp打印常量):{ {3}}

3 个答案:

答案 0 :(得分:7)

  

我认为以下声明地点之间没有太大区别:

这在很多方面都是错误的。

第一个声明污染了全局命名空间;你从未被使用过的名字“电视”,没有误解的可能性。这可能会导致阴影警告,它可能导致链接器错误,它可能会导致使用标头的任何人混淆。它也可能会导致与不使用标题的人发生问题,导致与其他人碰巧也会碰巧使用您的变量名称作为全局变量。

现代C ++中不推荐这种方法,但在C语言中无处不在,因此在.c文件(文件范围)中对“全局”变量使用静态关键字有很多用途。

第二个声明污染命名空间;这不是一个问题,因为命名空间可以自由地重新命名,并且可以免费制作。只要两个项目使用自己的,相对特定的命名空间,就不会发生冲突。在发生此类冲突的情况下,可以重命名每个冲突的名称空间以避免任何问题。

这是更现代的C ++ 03风格,而C ++ 11通过重命名模板大大扩展了这种策略。

第三种方法是结构,而不是类;它们有差异,特别是如果你想保持与C的兼容性。类范围复合对命名空间范围的好处;您不仅可以轻松地封装多个内容并使用特定名称,还可以通过方法和信息隐藏来增加封装,从而大大扩展代码的实用性。这主要是课程的好处,不论范围的好处。

你几乎肯定不会使用第一个,除非你的函数和变量非常宽泛,STL / STD就好,或者你的程序很小,不太可能被嵌入或重用。

现在让我们来看看你的案例。

  1. 构造函数的大小,如果它返回一个常量表达式,则不重要;所有代码都应该在编译时可执行。这意味着复杂性没有意义;它将始终编译为单个常量返回值。你几乎肯定不会使用C或D;所做的只是使constexpr的优化不起作用。我会使用A和B中的任何一个看起来更优雅,可能一个简单的赋值是A,复杂的常量表达式就是B.

  2. 这些都不一定是线程安全的;构造函数的内容将决定线程和异常安全性,并且很容易使这些语句中的任何一个都不是线程安全的。实际上,A最有可能是线程安全的;只要在调用main之前不访问该对象,它就应该完全形成;你的任何其他例子都不能说同样的话。至于你对B的分析,根据我的经验,大多数构造函数(特别是异常安全的构造函数)都很便宜,因为它们避免了分配。在这种情况下,您的任何案件之间不太可能存在太大差异。

  3. 我强烈建议您停止尝试这样的微优化,或者对C ++习语有更深刻的理解。你在这里尝试做的大多数事情不太可能导致性能的提高。

答案 1 :(得分:2)

你没有提到一个重要的选择:

namespace
{
    const T t = .....;
};

现在没有名称冲突问题。

如果T只是你想要构建一次,那么这是不合适的。但拥有一个庞大的全球&#34; object,const或not,是你真正想要避免的东西。它打破了封装,并在您的代码中引入了静态初始化顺序fiasco。

我从未需要过大的extern const对象。如果我需要一个大的硬编码查找表,那么我编写一个查找表的函数(可能作为类成员);并且该表是该单元的本地表,具有该功能的实现。

在我的代码中似乎需要一个大的非const全局对象,我实际上有一个函数,

namespace MyStuff
{
     T &get_global_T();
}

首次使用时构造对象。 (实际上,对象本身隐藏在一个单元中,T是一个指定接口的辅助类;所以我可以搞乱对象的细节而不会干扰使用它的任何代码)

答案 2 :(得分:1)

1

如果A,全局或命名空间范围(内部链接)和类范围(外部链接)之间存在差异。所以

// header.hpp
constexpr T A = <some value>; // internal linkage
namespace Nm { constexpr T A = <some value>; } // internal linkage
class Cl { public: static constexpr T A = <some value>; }; // not enough!

请考虑以下用法:

// user.cpp
std::cout << A << Nm::A << Cl::A; // ok
std::cout << &A << &Nm::A;        // ok
std::cout << &Cl::A;              // linker error: undefined reference to `Cl::A'

source.cpp 中放置Cl::A定义(除上述Cl::A声明之外)消除了此错误:

// source.cpp
constexpr T Cl::A;

外部链接意味着始终只有Cl::A的一个实例。所以Cl::A似乎是大T的非常好的候选者。但是:在这种情况下,我们可以确定静态初始化顺序fiasco不会出现吗?我相信答案是,因为Cl::A是在编译时构建的。

我已经在GNU / Linux平台上使用g ++ 4.8.2和4.9.0,clang ++ 3.4测试了ABa替代品。三个翻译单位的结果:

    source.cpp 中定义的类范围中的
  • A对fiasco都免疫,即使在编译时也在所有翻译单元中具有相同的地址。
  • 命名空间或全局范围内的
  • A对于大型数组和constexpr const char * A = "A";都有3个不同的地址(因为内部链接)。
  • 任何范围内的
  • Bstd::array<long double, 100>)都有2个不同的地址(地址在2个翻译单元中相同);另外所有3个B地址都提示了一些不同的内存位置(它们比其他地址大得多) - 我怀疑数组是在运行时复制到内存中的。
  • aconstexpr类型T一起使用时,例如intconst char *std::array,并在 source.cpp 中使用constexpr表达式初始化,与A一样好:免疫在所有翻译单位中惨败并拥有相同的地址。如果使用非constexpr初始化T类型constexpr的常量,例如std::time(nullptr),并在初始化之前使用,它将包含默认值(例如0的{​​{1}})。这意味着在这种情况下,常量值可以取决于静态初始化顺序。那么,使用非int值初始化a

底线

    在大多数情况下,
  1. 在类范围内更喜欢constexpr任何constexpr常量,因为它结合了完美的安全性,简单性,内存节省和性能。
  2. A(在 source.cpp 中使用a值初始化)应该使用,如果名称空间范围更好或者希望避免在标题中初始化.hpp (为了减少依赖关系和编译时间)。与constexpr相比,a有一个缺点:它只能在 source.cpp 中的编译时表达式中使用,并且只能在初始化之后使用。
  3. 在某些情况下,
  4. A应该用于小B:当命名空间范围更好或者需要模板编译时常量时(例如T)。当常量值很少使用或仅在特殊情况下使用时,也可以使用pi,例如B。错误消息。
  5. 其他替代品应该几乎不会被使用,因为它们很少比上述所有3种方式更适合。
      不应使用命名空间范围中的
    • A,因为它可能会导致 N 实例的常量,因此消耗sizeof(T) * N字节的内存并导致缓存未命中。这里 N 等于包含 header.hpp 的翻译单元数。如this proposal中所述,如果在内联函数中使用,则命名空间作用域中的A可能违反ODR。
    • C可用于大型TB通常更适合小型T),在两种罕见情况下:最好是函数调用;当命名空间作用域和标题初始化时更好。
    • 当在源文件中进行函数调用AND初始化时,可以使用
    • D
    • CAB相比的唯一缺点 - 其返回值不能用于编译时表达式。 D遭受同样的缺点和另一个缺点:函数调用运行时性能损失(因为它无法内联)。
  6. 2

    由于静态初始化顺序失败,避免使用非constexpr a。仅在确定瓶颈的情况下考虑a。否则,安全性比小的性能增益更重要。 bcd更加安全。但是cd有2个安全要求:

    for (auto f : {所有cd - 类似函数}) {

    • T构造函数不能调用f,因为如果静态局部变量的初始化递归地进入初始化变量的块,则行为是未定义的。这并不难确保。
    • 对于每个班级XX::~X调用f并且有一个静态初始化的X对象:X::X 必须致电f。原因是,static const T之后的f可以在全局X对象之后构造并因此被破坏;那么X::~X会导致UB。这个要求比前一个要求更难保证。所以它几乎禁止使用全局常量的复杂析构函数的全局或静态局部变量。如果静态初始化变量的析构函数不复杂,例如使用f()进行日志记录,然后将f();放在相应的构造函数中以确保安全。

    }

    注意:这两项要求不适用于CD

    • f的递归调用无法编译;
    • static constexpr T CD中的常量是在编译时构造的 - 在构造任何非平凡变量之前,所以它们在所有非平凡变量之后被破坏。破坏(以相反的顺序调用析构函数)。

    注意2:C++ FAQ建议使用cd的不同实现,但不强加第二个安全要求。然而,在这种情况下,静态常数永远不会被破坏,这会干扰内存泄漏检测,例如, Valgrind 诊断。应该避免内存泄漏,无论多么良性。因此,cd的这些修改版本只能在特殊情况下使用。

    这里考虑的另一个替代方案是具有内部联系的常量:

    // header.hpp
    namespace Ns { namespace { const T a1 = <some value>; } }
    

    此方法与命名空间范围中的A具有相同的大缺点:内部链接可以创建a1的副本数量,与包含 header.hpp 。它也可以像A一样违反ODR。但是,由于非constexpr的其他选项不如constexpr常量那么好,因此这种替代方法实际上可能有一些罕见的用途。 但是:这个&#34;解决方案&#34;在公共函数中使用a1时,仍然容易出现静态初始化顺序失败,而公共函数又用于初始化全局对象。所以引入内部联系并不能解决问题 - 只是隐藏它,使其不太可能,可能更难找到并修复。

    底线

    • c提供最佳性能并节省内存,因为它有助于重复使用一个T实例并且可以内联,因此在大多数情况下应该使用它。
    • dc一样可以节省内存,但性能更差,因为它永远不会被内联。但是d可用于减少编译时间。
    • 考虑b小类型或很少使用的常量(在很少使用的常量情况下,它的定义可以移动到 source.cpp 以避免在更改时重新编译)。如果bc的安全要求无法满足,d也是唯一的解决方案。如果经常使用常量,b绝对不适合大T,因为每次调用b时都必须构造常量。

    注意:在 header.hpp 中初始化的内联函数和变量还有另一个编译时问题。如果常量的定义依赖于在不同的头 bad.h 中声明的另一个常量,并且头 bad.h 不应该包含在 header.hpp中,然后Dda和修改后的b(定义移至 source.cpp )是唯一的选择