C ++ 11允许对非静态和非常量成员进行类内初始化。 什么改变了?

在C ++ 11之前,我们只能对整型或枚举types的静态常量成员进行类内初始化。 Stroustrup在他的C ++ FAQ中讨论了这个问题 ,举例如下:

class Y { const int c3 = 7; // error: not static static int c4 = 7; // error: not const static const float c5 = 7; // error: not integral }; 

以下推理:

那么为什么这些不便的限制存在? 一个类通常在头文件中声明,而头文件通常包含在许多翻译单元中。 但是,为了避免复杂的链接器规则,C ++要求每个对象都有唯一的定义。 如果C ++允许将需要作为对象存储在内存中的实体的类定义中断,那么该规则将被破坏。

但是,C ++ 11放宽了这些限制,允许非静态成员的类内初始化(§12.6.2/ 8):

在非委托构造函数中,如果给定的非静态数据成员或基类不是由mem-initializer-id指定的(包括由于构造函数没有ctor初始值设定项而没有mem-initializer-list的情况)而实体不是抽象类(10.4)的虚拟基类

  • 如果实体是一个非静态数据成员,它具有一个括号或等于初始值设定项 ,则该实体按照8.5中的规定进行初始化;
  • 否则,如果实体是变体成员(9.5),则不执行初始化;
  • 否则,实体将被默认初始化(8.5)。

9.4.2节还允许非常量静态成员的类初始化,如果它们用constexpr说明符标记的话。

那么我们在C ++ 03中有什么限制呢? 我们只是简单地接受“复杂的链接器规则”还是有其他的改变,这使得这更容易实现?

简单的答案是,他们保持链接器大致相同,代价是编译器比以前更加复杂。

也就是说,而不是由此产生多个链接器的定义,它仍然只是一个定义,编译器必须将其整理出来。

它也导致了程序员要更加复杂的规则来保持整理,但它很简单,没有什么大不了的。 当您为单个成员指定了两个不同的初始值设定项时,额外的规则会进入:

 class X { int a = 1234; public: X() = default; X(int z) : a(z) {} }; 

现在,此时的额外规则将处理使用非默认构造函数时用于初始化a值。 答案很简单:如果你使用一个没有指定任何其他值的构造函数,那么1234将被用来初始化a – 但是如果你使用一个指定其他值的构造函数,那么1234基本上是忽略。

例如:

 #include <iostream> class X { int a = 1234; public: X() = default; X(int z) : a(z) {} friend std::ostream &operator<<(std::ostream &os, X const &x) { return os << xa; } }; int main() { X x; X y{5678}; std::cout << x << "\n" << y; return 0; } 

结果:

 1234 5678 

我猜这个推理可能是在模板定稿之前写的。 在静态成员的类初始化器所需的所有“复杂链接器规则”之后,C ++ 11已经必须支持模板的静态成员。

考虑

 struct A { static int s = ::ComputeSomething(); }; // NOTE: This isn't even allowed, // thanks @Kapil for pointing that out // vs. template <class T> struct B { static int s; } template <class T> int B<T>::s = ::ComputeSomething(); // or template <class T> void Foo() { static int s = ::ComputeSomething(); s++; std::cout << s << "\n"; } 

编译器的问题在所有三种情况下是相同的:在哪个翻译单元应该发出s的定义和初始化它的代码? 简单的解决scheme是将其发送到任何地方,让链接器将其排除。 这就是为什么连接器已经支持像__declspec(selectany)这样的东西。 没有它就不可能实现C ++ 03。 这就是为什么没有必要扩展链接器。

说得更直白些:我认为旧标准的推理是错误的。


UPDATE

正如Kapil所指出的那样,我的第一个例子在当前的标准(C ++ 14)中是不允许的。 无论如何我留下了它,因为IMO是最难实施的(编译器,链接器)。 我的观点是:即使是这种情况,比使用模板时已经允许的情况还要困难。

理论上So why do these inconvenient restrictions exist?...理由是有效的,但它可以被轻易绕过,这正是C ++ 11所做的。

当你包含一个文件,它只是包含该文件,并忽略任何初始化。 只有在实例化类时才会初始化成员。

换句话说,初始化仍然与构造函数绑定,只是符号不同而且更方便。 如果构造函数未被调用,则值不会被初始化。

如果构造函数被调用,则这些值将在类中初始化(如果存在的话),或者构造函数可以用自己的初始化来覆盖它。 初始化的path基本上是相同的,即通过构造函数。

Stroustrup自己在C ++ 11上的常见问题就很明显。