【问题标题】:Why does it work when it breaks the rule of order of initialization list为什么它打破了初始化列表的顺序规则时会起作用
【发布时间】:2018-01-26 07:24:24
【问题描述】:

为什么这段代码有效?我预计这会因为违反基本 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 对象的引用。

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

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

【问题讨论】:

  • @JonathanPotter 引用不是指针。也许它被实现为指针,但即使总是这样,引用仍然不是指针
  • @VTT 没有理由这样做。
  • @VTT 对不起,我的意思是说没有有效理由这样做。还有have a read
  • @UKMonkey a(a) 是惯用的 C++。它可能在某些时候会引起某些人的困惑,但a(a_)确实现在为每个人添加了混乱。
  • @VTT 这里没有冲突。

标签: c++


【解决方案1】:

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

类似地,在对象的生命周期开始之前但在对象的生命周期开始之后 对象将占用的存储空间已被分配,或者在 对象的生命周期已经结束并且在存储之前 被占用的对象被重用或释放,任何引用 可以使用原始对象,但只能以有限的方式使用。对于一个对象 正在建设或销毁,请参阅 [class.cdtor]。否则,这样的 一个glvalue是指分配的存储 ([basic.stc.dynamic.deallocation]),并使用 不依赖于其值的 glvalue 是明确定义的。该程序 在以下情况下具有未定义的行为:

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

关于您的编辑:

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

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

【讨论】:

  • @StoryTeller - 请参阅我上面的编辑。
  • 所以当我在 B 的构造函数中添加对 a.doSth 的调用时,即使这样有效,它是未定义的行为吗?我明白了吗?
  • @YotKay - 完全正确。
  • 还有一件事:如果在 B 构造函数中我 push_back 我的 'a' 到向量,这也将是 UB?
  • @YotKay - 是的。 push_back 表示执行复制或移动,即调用成员函数。
【解决方案2】:

在您的代码 sn-p 中,当 C 正在构建时,a 尚未初始化但它已经在范围内,因此编译器不需要发出诊断。它的值是未定义的。

代码很好,因为B::aC::a 的正确别名。存储支持 C::a 的生命周期在 B::B() 运行时已经开始。

关于您的编辑:虽然C::a 的存储期限已经开始,但来自B::B()a.doSth() 绝对会导致未定义的行为(谷歌看看为什么某些东西可以是UB 并且仍然“工作”)。

【讨论】:

  • 但是我可以成功调用 'doSth' 方法并且代码有效。是 UB 吗?
  • 这是否意味着代码没问题?我的意思是除了有点混乱
  • @YotKay 现在你问我不确定存储对未初始化对象的引用是否是 UB;我不认为是。
  • 其值为未指定;尝试读取该值的程序具有未定义的行为
【解决方案3】:

这是因为您在 C::binitialization 期间没有访问未初始化的字段 C::a。通过调用C() : b(a),您将绑定对a 的引用,为B(A&amp; 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;
};

【讨论】:

    【解决方案4】:

    未定义的行为意味着一切皆有可能,包括看起来工作正常。并不意味着它会在下周甚至下一次运行它时正常工作 - 你可能会收到demons flying from your nose

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

    【讨论】:

      【解决方案5】:

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

      编译器不会阻止这种情况,但我想应该会。无论如何,我不认为这是 UB,除非您实际尝试使用未初始化的对象;只是存储参考应该没问题。

      【讨论】:

      • 是的,应该。至少我认为会。但不知何故,它没有。
      • 我现在用 Clang 和这里的结果一样,没有警告,没有错误。
      • @YotKay 我不认为编译器为这种情况发出任何东西;我的“应该”真的是一厢情愿。
      【解决方案6】:

      之所以有效,是因为 B 使用引用进行了初始化,并且该引用已经存在,因此可以使用它来初始化某些东西。

      如果您尝试在 B 的 ctor 中按值传递 a,那么编译器会抱怨:

      警告:字段“a”在此处使用时未初始化 [-Wuninitialized]

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-03-14
        • 2011-01-05
        • 1970-01-01
        • 2014-08-08
        • 1970-01-01
        • 2019-12-22
        • 1970-01-01
        • 2012-02-16
        相关资源
        最近更新 更多