C++ 11允许非静态和非const成员的类内初始化.改变了什么?

Jos*_*eld 80 c++ initialization class class-members c++11

在C++ 11之前,我们只能对整数或枚举类型的静态const成员执行类内初始化.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
};
Run Code Online (Sandbox Code Playgroud)

以下推理:

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

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

在非委托构造函数中,如果给定的非静态数据成员或基类未由mem-initializer-id指定(包括没有mem-initializer-list的情况,因为构造函数没有ctor-initializer)然后,实体不是抽象类(10.4)的虚基类

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

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

那么我们在C++ 03中受到限制的原因究竟发生了什么?我们只是简单地接受"复杂的链接器规则"或者是否有其他改变使得这更容易实现?

Jer*_*fin 62

简短的回答是,它们使链接器保持相同,但代价是使编译器比以前更复杂.

即,不是这导致链接器的多个定义要排序,它仍然只导致一个定义,并且编译器必须对其进行排序.

它也导致程序员更加复杂的规则,以保持整理,但它足够简单,这不是一个大问题.当您为单个成员指定了两个不同的初始值设定项时,会有额外的规则:

class X { 
    int a = 1234;
public:
    X() = default;
    X(int z) : a(z) {}
};
Run Code Online (Sandbox Code Playgroud)

现在,此时的额外规则处理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 << x.a;
    }
};

int main() { 
    X x;
    X y{5678};

    std::cout << x << "\n" << y;
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

结果:

1234
5678
Run Code Online (Sandbox Code Playgroud)

  • @allyourcode:是的,不是.是的,它使编译器编写更难.但不,因为它**使得编写C++规范变得相当困难. (8认同)

Pau*_*oke 8

我猜这个推理可能是在模板最终确定之前编写的.在所有"复杂的链接器规则"之后,静态成员的类内初始化程序已经/已经是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";
}
Run Code Online (Sandbox Code Playgroud)

编译器的问题在所有三种情况下都是相同的:在哪个翻译单元应该发出定义s和初始化它所需的代码?简单的解决方案是在任何地方发出它并让链接器对其进行排序.这就是为什么链接器已经支持了这样的东西__declspec(selectany).如果没有它,就不可能实现C++ 03.这就是没有必要扩展链接器的原因.

更坦率地说:我认为旧标准中给出的推理是完全错误的.


UPDATE

正如卡皮尔指出的那样,我的第一个例子甚至不允许在现行标准中使用(C++ 14).无论如何我离开了它,因为IMO是最难实现的(编译器,链接器).我的观点是:即使这种情况也不比使用模板时已经允许的情况更难.


zar*_*zar 7

从理论上讲,So why do these inconvenient restrictions exist?...原因是有效的,但可以轻松地绕开它,而这正是C ++ 11所做的。

当你一个文件,它只是包括文件和忽略任何初始化。仅在实例化类时才初始化成员。

换句话说,初始化仍然与构造函数联系在一起,只是表示法有所不同并且更加方便。如果未调用构造函数,则不会初始化值。

如果调用了构造函数,则使用类内初始化(如果存在)初始化值,或者构造函数可以使用自己的初始化覆盖这些值。初始化的路径本质上是相同的,即通过构造函数。

从Stroustrup自己在C ++ 11上的常见问题中可以明显看出这一点。