为什么当它打破初始化列表的顺序规则时它会起作用

Why does it work when it breaks the rule of order of initialization list

本文关键字:规则 顺序 起作用 列表 初始化 为什么      更新时间:2023-10-16

为什么这段代码有效?我预计这会失败,因为违反了基本的C++规则之一:

#include <iostream>
using namespace std;
struct A {
A() { cout << "ctor A" << endl; }
void doSth() { cout << "a doing sth" << endl; }
};
struct B {
B(A& a) : a(a) { cout << "ctor B" << endl; }
void doSth() { a.doSth(); }
A& a;
};
struct C {
C() : b(a) { cout << "ctor C" << endl; }
void doSth() { b.doSth(); }
B b;
A a;
};
int main()
{
C c;
c.doSth();
}

https://wandbox.org/permlink/aoJsYkbhDO6pNrg0

我预计这会失败,因为在 C 的构造函数中,当尚未创建此 A 对象时,B 会引用 A 的对象。

我错过了什么吗?初始化顺序规则是否与字段顺序相同,不适用于引用?

编辑: 更让我惊讶的是,我可以在 B 构造函数中添加对 "a.doSth();"的调用,这也将起作用。为什么?此时,A 对象应该不存在!

只要B的构造函数不使用它获得的引用来绑定其成员以外的任何内容,您的代码就可以了。当C的c'tor启动时,a的存储已经分配,就像Sneftel所说的那样,它在范围内。因此,您可以参考它,因为 [basic.life]/7 明确允许:

同样,在对象的生存期开始之前,但在 对象将占用的存储已分配,或者在 对象的生存期已经结束,并且在存储之前 占用的对象被重复使用或释放,任何引用 可以使用原始对象,但只能以有限的方式使用。对于对象 正在建设或破坏中,请参见[class.cdtor]。否则,这样的 GL值是指分配的存储 ([basic.stc.dynamic.deallocation]),并使用 不依赖于其值的glvalue是明确定义的。该计划 在以下情况下具有未定义的行为:

  • GL值用于访问对象,或
  • glvalue 用于调用对象的非静态成员函数,或者
  • GL值绑定到对虚拟基类([dcl.init.ref])的引用,或者
  • glvalue 用作 dynamic_cast 的操作数或 typeid 的操作数。

关于您的编辑:

更让我惊讶的是,我可以在 B 构造函数中添加对 "a.doSth();"的调用,这也将起作用。为什么?此时,A 对象应该不存在!

未定义的行为是未定义的。我链接到的段落中的第二个项目符号几乎说明了这一点。编译器可能足够聪明地捕捉到它,但并非必须如此。

在代码片段中,构造C时,a尚未初始化,但它已在范围内,因此编译器不需要发出诊断。其值未定义。

从某种意义上说,代码很好,因为B::a正确地是C::a的别名。存储支持C::a的生存期在B::B()运行时已经开始。

关于您的编辑:尽管C::a的存储持续时间已经开始,但B::B()a.doSth()绝对会导致未定义的行为(谷歌看看为什么某些东西可以是 UB 并且仍然"工作")。

这是因为您在初始化期间没有访问未初始化C::b字段C::a。通过调用C() : b(a)将引用绑定到要为构造函数提供的aB(A& a)。如果您以某种方式更改代码以实际使用未初始化的值,那么它将是一个未定义的行为:

struct B {
B(A& a)
: m_a(a) // now this calls copy constructor attempting to access uninitialized value of `a`
{ cout << "ctor B" << endl; }
void doSth() { a.doSth(); }
A m_a;
};

未定义的行为意味着一切皆有可能,包括看起来工作正常。这并不意味着它下周甚至下次运行它时都会正常工作 - 你可能会让恶魔从你的鼻子里飞出来。

当你调用a.doSth(),可能发生的事情是编译器将调用转换为静态a::doSth();因为它不是一个虚函数,所以它不需要访问对象来进行调用。函数本身不使用任何成员变量或函数,因此不会生成无效访问。即使不能保证有效,它也能工作。

它不"工作",因为用于初始化的a对象尚未调用其构造函数(您的日志显示) - 这意味着b的初始化可能会也可能不会失败,具体取决于a正在做什么。

编译器不会阻止这一点,但我想它应该。无论如何,我不认为这是UB,除非您实际尝试使用单元化对象;仅存储引用应该没问题。

它之所以有效B是因为它是用引用初始化的,并且该引用已经存在,因此可用于使用它初始化某些内容。

如果您尝试在 ctor 中按值传递aB则编译器会抱怨:

警告:此处使用字段"A"时未初始化 [-文初始化]