【发布时间】:2023-04-02 09:07:01
【问题描述】:
对于 C++ 类中的静态成员变量 - 初始化在类外完成。我想知道为什么?对此有任何逻辑推理/限制吗?或者它是纯粹的遗留实现 - 标准不想更正?
我认为在类中进行初始化更“直观”且不那么混乱。它还给人以变量的静态和全局性的感觉。例如,如果您看到静态 const 成员。
【问题讨论】:
标签: c++ initialization static-variables
对于 C++ 类中的静态成员变量 - 初始化在类外完成。我想知道为什么?对此有任何逻辑推理/限制吗?或者它是纯粹的遗留实现 - 标准不想更正?
我认为在类中进行初始化更“直观”且不那么混乱。它还给人以变量的静态和全局性的感觉。例如,如果您看到静态 const 成员。
【问题讨论】:
标签: c++ initialization static-variables
在 C++ 中,从一开始,initializer 的存在就是对象 definition 的专有属性,即带有初始化器的声明始终是 definition (几乎总是)。
您必须知道,C++ 程序中使用的每个外部对象都必须在一个翻译单元中定义一次且仅一次。允许静态对象的类内初始化程序将立即违反此约定:初始化程序将进入头文件(类定义通常驻留在其中)并因此生成同一静态对象的多个定义(一个用于包含头文件的每个翻译单元)。这当然是不可接受的。出于这个原因,静态类成员的声明方法完全是“传统的”:你只在头文件中声明它(即不允许初始化器),然后你定义 它在您选择的翻译单元中(可能带有初始化程序)。
此规则的一个例外是整型或枚举类型的 const 静态类成员,因为此类条目可以用于整型常量表达式 (ICE)。 ICE 的主要思想是它们在编译时被评估,因此不依赖于所涉及对象的定义。这就是为什么整数或枚举类型可能出现此异常的原因。但对于其他类型,它只会与 C++ 的基本声明/定义原则相矛盾。
【讨论】:
C++ 标准的第 9.4.2 节,静态数据成员:
如果
static数据成员是const整数或const枚举类型,它在类定义中的声明可以指定一个const-initializer,它应该是一个整数常量表达式。
因此,静态数据成员的值可能包含在“类中”(我假设您的意思是在类的声明中)。但是,静态数据成员的类型必须是const 整数或const 枚举类型。不能在类声明中指定其他类型的静态数据成员的值的原因是可能需要进行非平凡的初始化(即需要运行构造函数)。
想象一下,如果以下是合法的:
// my_class.hpp
#include <string>
class my_class
{
public:
static std::string str = "static std::string";
//...
与包含此标头的 CPP 文件相对应的每个目标文件不仅将拥有 my_class::str 的存储空间副本(由 sizeof(std::string) 字节组成),而且还有一个调用 std::string 的“ctor 部分”构造函数采用 C 字符串。 my_class::str 的存储空间的每个副本都将由一个公共标签标识,因此链接器理论上可以将存储空间的所有副本合并为一个副本。但是,链接器将无法隔离对象文件的 ctor 部分中构造函数代码的所有副本。这就像要求链接器删除所有代码以在以下编译中初始化str:
std::map<std::string, std::string> map;
std::vector<int> vec;
std::string str = "test";
int c = 99;
my_class mc;
std::string str2 = "test2";
编辑查看以下代码的 g++ 的汇编器输出很有启发性:
// SO4547660.cpp
#include <string>
class my_class
{
public:
static std::string str;
};
std::string my_class::str = "static std::string";
汇编代码可以通过执行得到:
g++ -S SO4547660.cpp
翻看g++生成的SO4547660.s文件,可以看到这么小的源文件有很多代码。
__ZN8my_class3strE是my_class::str的存储空间标签。还有一个__static_initialization_and_destruction_0(int, int) 函数的汇编源代码,它的标签为__Z41__static_initialization_and_destruction_0ii。该函数对 g++ 是特殊的,但只知道 g++ 将确保在执行任何非初始化程序代码之前调用它。请注意,此函数的实现调用__ZNSsC1EPKcRKSaIcE。这是std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&) 的错位符号。
回到上面的假设示例并使用这些详细信息,对应于包含my_class.hpp 的 CPP 文件的每个目标文件都将具有标签
__ZN8my_class3strE 用于 sizeof(std::string) 字节以及在其实现 __static_initialization_and_destruction_0(int, int) 函数时调用 __ZNSsC1EPKcRKSaIcE 的汇编代码。链接器可以轻松合并所有出现的__ZN8my_class3strE,但它不可能隔离在目标文件的__static_initialization_and_destruction_0(int, int) 实现中调用__ZNSsC1EPKcRKSaIcE 的代码。
【讨论】:
class my_class { public: static const double pi = 3.14; };
const整数或const枚举类型的静态数据成员的值可以在声明中指定的相同原因。我不知道为什么不是。
const double 和 const float “不受支持”。如果支持这些类型,则 C++ 编译器必须能够评估“浮点常量表达式”。例如,static const int i = 44 << 6 ^ 0x63ab9900; 是允许的,因此编译器必须能够计算常量整数表达式。如果static const float f = 24.382f * -999.283f 也被允许,那么 C++ 编译器必须具有计算浮点运算的函数。这可能已被 C++ 委员会视为不必要的复杂情况。
从根本上说,这是因为静态成员必须在一个翻译单元中定义,以免违反One-Definition Rule。如果语言允许类似:
struct Gizmo
{
static string name = "Foo";
};
然后name 将在#includes 这个头文件的每个翻译单元中定义。
C++ 允许您在声明中定义 integral 静态成员,但您仍然必须在单个翻译单元中包含定义,但这只是一种快捷方式,或语法糖。所以,这是允许的:
struct Gizmo
{
static const int count = 42;
};
只要 a) 表达式是 const 整数或枚举类型,b) 表达式可以在编译时求值,c) 在不违反单一定义规则的某处仍有定义:
文件:gizmo.cpp
#include "gizmo.h"
const int Gizmo::count;
【讨论】:
Gizmo 示例是合法的,我认为它不会违反单一定义规则,因为每个翻译单元将 有一个Gizmo::name 的单一定义。
我认为在class 块之外进行初始化的主要原因是允许使用其他类成员函数的返回值进行初始化。如果您想用b::some_static_fn() 初始化a::var,您需要确保每个包含a.h 的.cpp 文件首先包含b.h。这将是一团糟,尤其是当您(迟早)遇到循环引用时,您只能使用其他不必要的interface 来解决。同样的问题是在.cpp 文件中包含类成员函数实现而不是将所有内容都放在主类'.h 中的主要原因。
至少对于成员函数,您确实可以选择在标题中实现它们。对于变量,您必须在 .cpp 文件中进行初始化。我不太同意这种限制,我也不认为有充分的理由。
【讨论】:
这是因为代码的编译方式。如果您要在通常位于标头中的类中对其进行初始化,则每次包含标头时,您都会获得静态变量的实例。这绝对不是本意。在类之外对其进行初始化,您可以在 cpp 文件中对其进行初始化。
【讨论】:
statics 的一个要求是所有翻译单元使用一个定义。