对于 C++ 类中的静态成员变量 - 初始化是在类外部完成的。我想知道为什么?对此有何逻辑推理/约束?或者它纯粹是遗留实现——标准不想纠正它?
我认为在类中进行初始化更“直观”并且更不易混淆。它还给出了变量的静态和全局性的感觉。例如,如果您看到 static const 成员。
For static member variables in C++ class - the initialization is done outside the class. I wonder why? Any logical reasoning/constraint for this? Or is it purely legacy implementation - which the standard does not want to correct?
I think having initialization in the class is more "intuitive" and less confusing.It also gives the sense of both static and global-ness of the variable. For example if you see the static const member.
发布评论
评论(5)
从根本上来说,这是因为静态成员必须在一个翻译单元中定义,以免违反 单一定义规则。如果语言允许类似以下内容:
那么
name
将在#include
作为此头文件的每个翻译单元中定义。C++ 确实允许您在声明中定义完整静态成员,但您仍然必须在单个翻译单元中包含定义,但这只是一个快捷方式或语法糖。因此,这是允许的:
只要 a) 表达式是 const 整型或枚举类型,b) 表达式可以在编译时求值,并且 c) 仍然有一个定义不存在不要违反单一定义规则:
文件:gizmo.cpp
Fundamentally this is because static members must be defined in exactly one translation unit, in order to not violate the One-Definition Rule. If the language were to allow something like:
then
name
would be defined in each translation unit that#include
s this header file.C++ does allow you to define integral static members within the declaration, but you still have to include a definition within a single translation unit, but this is just a shortcut, or syntactic sugar. So, this is allowed:
So long as a) the expression is
const
integral or enumeration type, b) the expression can be evaluated at compile-time, and c) there is still a definition somewhere that doesn't violate the one definition rule:file: gizmo.cpp
在 C++ 中,自古以来,初始化器的存在就是对象定义的独占属性,即带有初始化器的声明始终是定义 >(几乎总是)。
正如您必须知道的,C++ 程序中使用的每个外部对象都必须在一个翻译单元中定义一次且仅一次。允许静态对象的类内初始化程序将立即违反这一约定:初始化程序将进入头文件(类定义通常驻留在其中),从而生成同一静态对象的多个定义(每个包含头文件的翻译单元一个) )。这当然是不可接受的。因此,静态类成员的声明方法完全是“传统”的:您只需在头文件中声明它(即不允许初始化程序),然后定义它> 它在您选择的翻译单元中(可能带有初始值设定项)。
此规则的一个例外是整型或枚举类型的 const 静态类成员,因为此类条目可用于整型常量表达式 (ICE)。 ICE 的主要思想是它们在编译时进行评估,因此不依赖于所涉及对象的定义。这就是为什么整型或枚举类型可能出现此异常的原因。但对于其他类型,它只会与 C++ 的基本声明/定义原则相矛盾。
In C++ since the beginning of times the presence of an initializer was an exclusive attribute of object definition, i.e. a declaration with an initializer is always a definition (almost always).
As you must know, each external object used in C++ program has to be defined once and only once in only one translation unit. Allowing in-class initializers for static objects would immediately go against this convention: the initializers would go into header files (where class definitions usually reside) and thus generate multiple definitions of the same static object (one for each translation unit that includes the header file). This is, of course, unacceptable. For this reason, the declaration approach for static class members is left perfectly "traditional": you only declare it in the header file (i.e. no initializer allowed), and then you define it in a translation unit of your choice (possibly with an initializer).
One exception from this rule was made for const static class members of integral or enum types, because such entries can for Integral Constant Expressions (ICEs). The main idea of ICEs is that they are evaluated at compile time and thus do no depend on definitions of the objects involved. Which is why this exception was possible for integral or enum types. But for other types it would just contradict the basic declaration/definition principles of C++.
这是因为代码的编译方式。如果您要在类中初始化它(通常位于标头中),则每次包含标头时,您都会获得静态变量的实例。这绝对不是本意。在类外部对其进行初始化使您可以在 cpp 文件中对其进行初始化。
It's because of the way the code is compiled. If you were to initialize it in the class, which often is in the header, every time the header is included you'd get an instance of the static variable. This is definitely not the intent. Having it initialized outside the class gives you the possibility to initialize it in the cpp file.
C++ 标准的第 9.4.2 节“静态数据成员”指出:
因此,静态数据成员的值可能包含在“类内”(我认为您的意思是在类的声明内)。但是,静态数据成员的类型必须是
const
整型或const
枚举类型。无法在类声明中指定其他类型的静态数据成员的值的原因是可能需要进行重要的初始化(即需要运行构造函数)。想象一下,如果以下内容是合法的:
与包含此标头的 CPP 文件对应的每个对象文件不仅具有
my_class::str
存储空间的副本(由sizeof(std: :string)
字节),但也是一个“ctor 部分”,它调用采用 C 字符串的std::string
构造函数。my_class::str
存储空间的每个副本都将由一个公共标签来标识,因此链接器理论上可以将存储空间的所有副本合并为一个副本。但是,链接器无法隔离对象文件的构造函数部分中构造函数代码的所有副本。这就像要求链接器删除以下编译中初始化str
的所有代码:编辑 查看 g++ 的汇编器输出是有启发性的如下代码:
可以通过执行得到汇编代码:
查看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回到上面的假设示例并使用这些详细信息,与包含
my_class.hpp
的 CPP 文件对应的每个目标文件都将具有标签__ZN8my_class3strE
用于sizeof(std::string)
字节以及在其实现__static_initialization_and_destruction_0(int , int)
函数。链接器可以轻松合并所有出现的__ZN8my_class3strE
,但它不可能在__static_initialization_and_destruction_0(int, int)< 的目标文件实现中隔离调用
__ZNSsC1EPKcRKSaIcE
的代码。 /代码>。Section 9.4.2, Static data members, of the C++ standard states:
Therefore, it is possible for the value of a static data member to be included "within the class" (by which I presume that you mean within the declaration of the class). However, the type of the static data member must be a
const
integral orconst
enumeration type. The reason why the values of static data members of other types cannot be specified within the class declaration is that non-trivial initialization is likely required (that is, a constructor needs to run).Imagine if the following were legal:
Each object file corresponding to CPP files that include this header would not only have a copy of the storage space for
my_class::str
(consisting ofsizeof(std::string)
bytes), but also a "ctor section" that calls thestd::string
constructor taking a C-string. Each copy of the storage space formy_class::str
would be identified by a common label, so a linker could theoretically merge all copies of the storage space into a single one. However, a linker would not be able to isolate all copies of the constructor code within the object files' ctor sections. It would be like asking the linker to remove all of the code to initializestr
in the compilation of the following:EDIT It is instructive to look at the assembler output of g++ for the following code:
The assembly code can be obtained by executing:
Looking through the
SO4547660.s
file that g++ generates, you can see that there is a lot of code for such a small source file.__ZN8my_class3strE
is the label of the storage space formy_class::str
. There is also the assembly source of a__static_initialization_and_destruction_0(int, int)
function, which has the label__Z41__static_initialization_and_destruction_0ii
. That function is special to g++ but just know that g++ will make sure that it gets called before any non-initializer code gets executed. Notice that the implementation of this function calls__ZNSsC1EPKcRKSaIcE
. This is the mangled symbol forstd::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&)
.Going back to the hypothetical example above and using these details, each object file corresponding to a CPP file that includes
my_class.hpp
would have the label__ZN8my_class3strE
forsizeof(std::string)
bytes as well as assembly code to call__ZNSsC1EPKcRKSaIcE
within its implementation of the__static_initialization_and_destruction_0(int, int)
function. The linker can easily merge all occurrences of__ZN8my_class3strE
, but it cannot possibly isolate the code that calls__ZNSsC1EPKcRKSaIcE
within the object file's implementation of__static_initialization_and_destruction_0(int, int)
.我认为在 class 块之外完成初始化的主要原因是允许使用其他类成员函数的返回值进行初始化。如果您想使用
b::some_static_fn()
初始化a::var
,您需要确保每个.cpp
文件首先包括ah
包括bh
。这会很混乱,特别是当(迟早)您遇到循环引用时,您只能使用不必要的接口
来解决。同样的问题是在.cpp
文件中实现类成员函数而不是将所有内容都放在主类的.h
中的主要原因。至少对于成员函数,您可以选择在标头中实现它们。对于变量,您必须在 .cpp 文件中进行初始化。我不太同意这种限制,而且我认为也没有充分的理由。
I think the main reason to have initialization done outside the
class
block is to allow for initialization with return values of other class member functions. If you wanted to intializea::var
withb::some_static_fn()
you'd need to make sure that every.cpp
file that includesa.h
includesb.h
first. It'd be a mess, especially when (sooner or later) you run into a circular reference that you could only resolve with an otherwise unnecessaryinterface
. The same issue is the main reason for having class member function implementations in a.cpp
file instead of putting everything in your main class'.h
.At least with member functions you do have the option to implement them in the header. With variables you must do the initialization in a .cpp file. I don't quite agree with the limitation, and I don't think there's a good reason for it either.