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


87

在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-initializer而没有mem-initializer-list的情况)并且该实体不是抽象类(10.4)的虚拟基类,则

  • 如果实体是具有brace-or-equal-initializer的非静态数据成员,则按照8.5中的指定进行初始化;
  • 否则,如果实体是变量成员(9.5),则不执行初始化。
  • 否则,该实体为默认初始化(8.5)。

9.4.2节还允许非常量静态成员的类内初始化(如果用说明constexpr符标记)。

那么,我们在C ++ 03中受到限制的原因发生了什么?我们是否只是接受“复杂的链接器规则”,还是进行了其他更改以使其更容易实现?


5
什么都没发生。通过所有这些仅标头模板,编译器变得越来越聪明,因此现在相对容易扩展。
OO Tiib

有趣的是,当我选择C ++ 11之前的编译版本时,我可以初始化非静态const整数成员
Dean P

Answers:


67

简短的答案是,它们使链接程序保持相同,但代价是使编译器比以前更加复杂。

即,不是导致链接器要定义多个定义,而是仍然只生成一个定义,并且编译器必须对它进行排序。

这也导致程序员要整理出一些更复杂的规则,但是它非常简单,所以没什么大不了的。当您为单个成员指定了两个不同的初始化程序时,就会出现额外的规则:

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 << x.a;
    }
};

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

    std::cout << x << "\n" << y;
    return 0;
}

结果:

1234
5678

1
似乎以前很有可能这样。这只是使编写编译器的工作更加困难。这是一个公平的声明吗?
allyourcode

10
@allyourcode:是和否。是的,这使得编写编译器更加困难。但是没有,因为这使编写C ++规范变得更加困难。
杰里·科芬

如何初始化类成员有区别:int x = 7; 或int x {7} ;?
mbaros

9

我猜想推理可能是在模板完成之前编写的。在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以及对其进行初始化所需的代码?一种简单的解决方案是将其发送到任何地方,然后让链接程序对其进行整理。这就是链接器已经支持诸如之类的原因的原因__declspec(selectany)。没有它,就不可能实现C ++ 03。这就是为什么没有必要扩展链接器的原因。

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


更新

正如Kapil指出的那样,当前标准(C ++ 14)甚至不允许我的第一个示例。无论如何,我都保留了它,因为IMO是实现过程中最困难的情况(编译器,链接器)。我的观点是:即使是这种情况,也没有比例如使用模板时所允许的情况难。


遗憾的是,它没有得到任何支持,因为许多C ++ 11功能都相似,因为编译器已经包含了必要的功能或优化。
亚历克斯·

@AlexCourt我最近写了这个答案。问题和杰里的答案来自2012年。所以我想这就是为什么我的回答没有引起足够重视的原因。
保罗·格罗

1
这将不符合“ struct A {static int s = :: ComputeSomething();}”,因为只能在类中初始化静态const
PapaDiHatti

8

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

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

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

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

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


关于“如果未调用构造函数,则不会初始化值”:如何Y::c3解决问题中的成员初始化?据我了解,c3除非有构造函数覆盖声明中给出的默认值,否则它将始终被初始化。
彼得-恢复莫妮卡
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.