c++静态成员变量及其初始化

时间:2021-09-04 22:46:11


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?

对于c++类中的静态成员变量——初始化是在类之外完成的。我想知道为什么?有什么逻辑推理/约束吗?或者是纯粹的遗留实现——标准不希望对其进行纠正?

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.

我认为在类中进行初始化更“直观”,也更不容易混淆。它还提供了变量的静态和全局性的感觉。例如,如果您看到静态const成员。

5 个解决方案

#1


37  

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:

从根本上说,这是因为静态成员必须在一个翻译单元中定义,以避免违反一个定义规则。如果语言允许:

struct Gizmo
{
  static string name = "Foo";
};

then name would be defined in each translation unit that #includes 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:

c++确实允许您在声明中定义完整的静态成员,但是您仍然需要在一个翻译单元中包含一个定义,但是这只是一个快捷方式,或者语法糖。所以,这是允许的:

struct Gizmo
{
  static const int count = 42;
};

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:

只要a)表达式是const积分或枚举类型,b)表达式可以在编译时进行计算,c)在某个地方仍然有一个不违反一个定义规则的定义:

file: gizmo.cpp

文件:gizmo.cpp

#include "gizmo.h"

const int Gizmo::count;

#2


11  

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).

在c++中,从一开始,初始化器的存在就是对象定义的唯一属性,即具有初始化器的声明始终是一个定义(几乎总是)。

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).

您必须知道,在c++程序中使用的每个外部对象必须在一个翻译单元中定义一次,并且只能定义一次。对于静态对象,允许类内初始化器会立即违背这个约定:初始化器将进入头文件(类定义通常驻留在头文件中),从而生成同一个静态对象的多个定义(每个转换单元包含头文件)。当然,这是不可接受的。由于这个原因,静态类成员的声明方法被保留为完美的“传统”:您只在头文件中声明它(即不允许初始化),然后在您选择的翻译单元中定义它(可能使用初始化)。

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++.

这条规则的一个例外是对整型或枚举类型的const静态类成员,因为这样的条目可以用于整型常量表达式(ICEs)。ICEs的主要思想是在编译时进行计算,因此不依赖于所涉及对象的定义。这就是为什么这个异常对于整数类型或枚举类型是可能的。但对于其他类型,它只会与c++的基本声明/定义原则相矛盾。

#3


2  

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.

这是因为代码的编译方式。如果要在类中初始化它(通常在header中),每次包含header时都会得到静态变量的实例。这绝对不是目的。在类之外初始化它可以在cpp文件中初始化它。

#4


1  

Section 9.4.2, Static data members, of the C++ standard states:

第9.4.2节,c++标准状态的静态数据成员:

If a static data member is of const integral or const enumeration type, its declaration in the class definition can specify a const-initializer which shall be an integral constant expression.

如果静态数据成员属于const积分或const枚举类型,那么它在类定义中的声明可以指定一个const-initializer,它应该是一个完整的常量表达式。

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 or const 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).

因此,可以将静态数据成员的值包含在“类中”(我假定您的意思是在类的声明中)。但是,静态数据成员的类型必须是const整数或const枚举类型。在类声明中不能指定其他类型的静态数据成员的值的原因是可能需要非平凡的初始化(也就是说,需要运行构造函数)。

Imagine if the following were legal:

想象一下,如果以下是合法的:

// my_class.hpp
#include <string>

class my_class
{
public:
  static std::string str = "static std::string";
//...

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 of sizeof(std::string) bytes), but also a "ctor section" that calls the std::string constructor taking a C-string. Each copy of the storage space for my_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 initialize str in the compilation of the following:

每个与包含此头的CPP文件对应的对象文件不仅有my_class::str(由sizeof(std::string)字节组成)的存储空间的副本,而且还有一个“ctor节”,该节调用std::string构造函数获取C-string。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";

EDIT It is instructive to look at the assembler output of g++ for the following code:

编辑以下代码,查看g++的汇编程序输出是有意义的:

// SO4547660.cpp
#include <string>

class my_class
{
public:
    static std::string str;
};

std::string my_class::str = "static std::string";

The assembly code can be obtained by executing:

可通过执行:

g++ -S SO4547660.cpp

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.

透过SO4547660。g++生成的s文件,您可以看到对于这么小的源文件有很多代码。

__ZN8my_class3strE is the label of the storage space for my_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 for std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&).

__ZN8my_class3strE是my_class::str存储空间的标签。还有一个__static_initialization_and_销毁_0(int, int)函数的组装源,它的标签是__z41__static_initialization_and_析构_0ii。这个函数对于g++是特殊的,但是要知道,g++会确保在执行任何非初始化代码之前调用它。注意,这个函数的实现调用__ZNSsC1EPKcRKSaIcE。这是std:::basic_string , std::allocator >::basic_string(char const*, std:::allocator const&st)的损坏符号。 ,>

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 for sizeof(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).

回到上面的假设示例并使用这些细节,每个对象文件对应于包含my_class的CPP文件。hpp将具有sizeof(std::string)字节的标签__zn8_myclass3 stre,以及在__static_initialization_and_析构函数(int, int)的实现中调用__ZNSsC1EPKcRKSaIcE的汇编代码。链接器可以很容易地合并所有出现的__ZN8my_class3strE,但是它不可能将调用__ZNSsC1EPKcRKSaIcE的代码隔离在对象文件的__static_initialization_and_析构_析构_0(int, int)实现中。

#5


0  

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 intialize a::var with b::some_static_fn() you'd need to make sure that every .cpp file that includes a.h includes b.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 unnecessary interface. 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.

我认为在类块之外进行初始化的主要原因是允许使用其他类成员函数的返回值进行初始化。如果您想用b:::some_static_fn()将a::var与b:::some_static_fn()结合使用,您需要确保包含a的每个.cpp文件。h包括b。h。这将是一个混乱,特别是当(迟早)您遇到一个循环引用时,您只能使用一个不必要的接口来解决它。同样的问题是在.cpp文件中实现类成员函数的主要原因,而不是将所有内容都放到主类' .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.

至少对于成员函数,您可以在header中实现它们。对于变量,您必须在.cpp文件中进行初始化。我不太同意这种限制,我也不认为有什么好的理由。

#1


37  

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:

从根本上说,这是因为静态成员必须在一个翻译单元中定义,以避免违反一个定义规则。如果语言允许:

struct Gizmo
{
  static string name = "Foo";
};

then name would be defined in each translation unit that #includes 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:

c++确实允许您在声明中定义完整的静态成员,但是您仍然需要在一个翻译单元中包含一个定义,但是这只是一个快捷方式,或者语法糖。所以,这是允许的:

struct Gizmo
{
  static const int count = 42;
};

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:

只要a)表达式是const积分或枚举类型,b)表达式可以在编译时进行计算,c)在某个地方仍然有一个不违反一个定义规则的定义:

file: gizmo.cpp

文件:gizmo.cpp

#include "gizmo.h"

const int Gizmo::count;

#2


11  

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).

在c++中,从一开始,初始化器的存在就是对象定义的唯一属性,即具有初始化器的声明始终是一个定义(几乎总是)。

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).

您必须知道,在c++程序中使用的每个外部对象必须在一个翻译单元中定义一次,并且只能定义一次。对于静态对象,允许类内初始化器会立即违背这个约定:初始化器将进入头文件(类定义通常驻留在头文件中),从而生成同一个静态对象的多个定义(每个转换单元包含头文件)。当然,这是不可接受的。由于这个原因,静态类成员的声明方法被保留为完美的“传统”:您只在头文件中声明它(即不允许初始化),然后在您选择的翻译单元中定义它(可能使用初始化)。

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++.

这条规则的一个例外是对整型或枚举类型的const静态类成员,因为这样的条目可以用于整型常量表达式(ICEs)。ICEs的主要思想是在编译时进行计算,因此不依赖于所涉及对象的定义。这就是为什么这个异常对于整数类型或枚举类型是可能的。但对于其他类型,它只会与c++的基本声明/定义原则相矛盾。

#3


2  

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.

这是因为代码的编译方式。如果要在类中初始化它(通常在header中),每次包含header时都会得到静态变量的实例。这绝对不是目的。在类之外初始化它可以在cpp文件中初始化它。

#4


1  

Section 9.4.2, Static data members, of the C++ standard states:

第9.4.2节,c++标准状态的静态数据成员:

If a static data member is of const integral or const enumeration type, its declaration in the class definition can specify a const-initializer which shall be an integral constant expression.

如果静态数据成员属于const积分或const枚举类型,那么它在类定义中的声明可以指定一个const-initializer,它应该是一个完整的常量表达式。

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 or const 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).

因此,可以将静态数据成员的值包含在“类中”(我假定您的意思是在类的声明中)。但是,静态数据成员的类型必须是const整数或const枚举类型。在类声明中不能指定其他类型的静态数据成员的值的原因是可能需要非平凡的初始化(也就是说,需要运行构造函数)。

Imagine if the following were legal:

想象一下,如果以下是合法的:

// my_class.hpp
#include <string>

class my_class
{
public:
  static std::string str = "static std::string";
//...

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 of sizeof(std::string) bytes), but also a "ctor section" that calls the std::string constructor taking a C-string. Each copy of the storage space for my_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 initialize str in the compilation of the following:

每个与包含此头的CPP文件对应的对象文件不仅有my_class::str(由sizeof(std::string)字节组成)的存储空间的副本,而且还有一个“ctor节”,该节调用std::string构造函数获取C-string。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";

EDIT It is instructive to look at the assembler output of g++ for the following code:

编辑以下代码,查看g++的汇编程序输出是有意义的:

// SO4547660.cpp
#include <string>

class my_class
{
public:
    static std::string str;
};

std::string my_class::str = "static std::string";

The assembly code can be obtained by executing:

可通过执行:

g++ -S SO4547660.cpp

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.

透过SO4547660。g++生成的s文件,您可以看到对于这么小的源文件有很多代码。

__ZN8my_class3strE is the label of the storage space for my_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 for std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&).

__ZN8my_class3strE是my_class::str存储空间的标签。还有一个__static_initialization_and_销毁_0(int, int)函数的组装源,它的标签是__z41__static_initialization_and_析构_0ii。这个函数对于g++是特殊的,但是要知道,g++会确保在执行任何非初始化代码之前调用它。注意,这个函数的实现调用__ZNSsC1EPKcRKSaIcE。这是std:::basic_string , std::allocator >::basic_string(char const*, std:::allocator const&st)的损坏符号。 ,>

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 for sizeof(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).

回到上面的假设示例并使用这些细节,每个对象文件对应于包含my_class的CPP文件。hpp将具有sizeof(std::string)字节的标签__zn8_myclass3 stre,以及在__static_initialization_and_析构函数(int, int)的实现中调用__ZNSsC1EPKcRKSaIcE的汇编代码。链接器可以很容易地合并所有出现的__ZN8my_class3strE,但是它不可能将调用__ZNSsC1EPKcRKSaIcE的代码隔离在对象文件的__static_initialization_and_析构_析构_0(int, int)实现中。

#5


0  

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 intialize a::var with b::some_static_fn() you'd need to make sure that every .cpp file that includes a.h includes b.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 unnecessary interface. 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.

我认为在类块之外进行初始化的主要原因是允许使用其他类成员函数的返回值进行初始化。如果您想用b:::some_static_fn()将a::var与b:::some_static_fn()结合使用,您需要确保包含a的每个.cpp文件。h包括b。h。这将是一个混乱,特别是当(迟早)您遇到一个循环引用时,您只能使用一个不必要的接口来解决它。同样的问题是在.cpp文件中实现类成员函数的主要原因,而不是将所有内容都放到主类' .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.

至少对于成员函数,您可以在header中实现它们。对于变量,您必须在.cpp文件中进行初始化。我不太同意这种限制,我也不认为有什么好的理由。