ChatGPT解决这个技术问题 Extra ChatGPT

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

在 C++11 之前,我们只能对整型或枚举类型的静态 const 成员进行类内初始化。 Stroustrup discusses this in his 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) 的虚拟基类,则如果实体是具有大括号或等号初始化器的非静态数据成员,则按照 8.5 中的规定初始化实体;否则,如果实体是变体成员(9.5),则不执行初始化;否则,实体被默认初始化(8.5)。

如果使用 constexpr 说明符标记非常量静态成员,第 9.4.2 节还允许对它们进行类内初始化。

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

没啥事儿。使用所有这些仅包含标头的模板,编译器变得更加智能,因此现在扩展相对容易。
有趣的是,在我的 IDE 上,当我选择预 C++11 编译时,我可以初始化非静态 const 积分成员

J
Jerry Coffin

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

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

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

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

似乎这在以前是完全可能的。它只是让编写编译器的工作变得更加困难。这是一个公平的说法吗?
@allyourcode:是也不是。是的,它使编写编译器变得更加困难。但是不,因为它也使编写 C++ 规范变得相当困难。
如何初始化类成员是否有区别: int x=7;或 int x{7};?
P
Paul Groke

我猜推理可能是在模板最终确定之前编写的。毕竟,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 年开始的。所以我想这就是为什么我的回答没有受到太多关注的原因。
这将不符合“struct A { static int s = ::ComputeSomething(); }”,因为只能在类中初始化静态 const
z
zar

理论上 So why do these inconvenient restrictions exist?... 原因是有效的,但它很容易被绕过,而这正是 C++ 11 所做的。

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

也就是说,初始化还是和构造函数绑定的,只是记法不同,更方便。如果未调用构造函数,则不会初始化值。

如果调用构造函数,则使用类内初始化(如果存在)初始化值,或者构造函数可以使用自己的初始化覆盖它。初始化的路径本质上是一样的,都是通过构造函数。

这从 Stroustrup 自己在 C++ 11 上的 FAQ 可以看出。


回复“如果不调用构造函数,则不初始化值”:问题中如何规避Y::c3的成员初始化?据我了解,c3 将始终被初始化,除非有一个构造函数覆盖声明中给出的默认值。