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
};

理由如下:

那么,为什么会存在这些不便的限制呢?类通常在头文件中声明,而头文件通常包含在许多翻译单元中。但是,为了避免复杂的链接器规则,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中受到限制的原因是什么呢?我们只是简单地接受“复杂的链接器规则”,还是改变了一些其他的东西,使其更容易实现?

108076 次浏览

The short answer is that they kept the linker about the same, at the expense of making the compiler still more complicated than previously.

I.e., instead of this resulting in multiple definitions for the linker to sort out, it still only results in one definition, and the compiler has to sort it out.

It also leads to somewhat more complex rules for the programmer to keep sorted out as well, but it's mostly simple enough that it's not a big deal. The extra rules come in when you have two different initializers specified for a single member:

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

Now, the extra rules at this point deal with what value is used to initialize a when you use the non-default constructor. The answer to that is fairly simple: if you use a constructor that doesn't specify any other value, then the 1234 would be used to initialize a -- but if you use a constructor that specifies some other value, then the 1234 is basically ignored.

For example:

#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;
}

Result:

1234
5678

I guess that reasoning might have been written before templates were finalized. After all the "complicated linker rule(s)" necessary for in-class initializers of static members was/were already necessary for C++11 to support static members of templates.

Consider

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";
}

The problem for the compiler is the same in all three cases: in which translation-unit should it emit the definition of s and the code necessary to initialize it? The simple solution is to emit it everywhere and let the linker sort it out. That's why the linkers already supported things like __declspec(selectany). It just wouldn't have been possible to implement C++03 without it. And that's why it wasn't necessary to extend the linker.

To put it more bluntly: I think the reasoning given in the old standard is just plain wrong.


UPDATE

As Kapil pointed out, my first example isn't even allowed in the current standard (C++14). I left it in anyway, because it IMO is the hardest case for the implementation (compiler, linker). My point is: even that case is not any harder than what's already allowed e.g. when using templates.

In theory So why do these inconvenient restrictions exist?... reason is valid but it can rather be easily bypassed and this is exactly what C++ 11 does.

When you include a file, it simply includes the file and disregards any initialization. The members are initialized only when you instantiate the class.

In other words, the initialization is still tied with constructor, just the notation is different and is more convenient. If the constructor is not called, the values are not initialized.

If the constructor is called, the values are initialized with in-class initialization if present or the constructor can override that with own initialization. The path of initialization is essentially the same, that is, via the constructor.

This is evident from Stroustrup own FAQ on C++ 11.