为什么它在违反初始化列表的规则时会起作用

时间:2018-01-25 11:04:11

标签: c++

为什么这段代码有效?由于打破了一个基本的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对象不应该存在!

6 个答案:

答案 0 :(得分:25)

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

  

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

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

关于你的编辑:

  

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

未定义未定义的行为。我链接的段落中的第二个子弹几乎都说了。编译器可能足够聪明以捕获它,但它不一定是。

答案 1 :(得分:11)

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

代码很好,因为B::a恰好是C::a的别名。存储后备C::a的生命周期已经在B::B()运行时开始。

关于您的修改:虽然C::a的存储时间已经开始,但来自a.doSth()的{​​{1}}绝对会导致未定义的行为(谷歌会看到为什么某些内容可能是UB和仍然“工作”)。

答案 2 :(得分:1)

这是有效的,因为您在C::a初始化期间未访问未初始化的字段C::b。通过调用C() : b(a),您绑定了对a构造函数提供的B(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;
};

答案 3 :(得分:1)

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

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

答案 4 :(得分:0)

它没有&#34;工作&#34;在某种意义上,用于初始化的a对象尚未调用其构造函数(您的日志显示) - 这意味着b的init可能会或可能不会失败,具体取决于a正在做。

编译器并没有阻止它,但我想它应该。无论如何,我不认为这是UB,除非你真的试图使用单元化对象;只是存储引用应该没问题。

答案 5 :(得分:0)

它的工作原理是因为B是用引用初始化的,并且该引用已经存在,因此它可以用来初始化它。

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

  

警告:字段&#39; a&#39;在这里使用时未初始化         [-Wuninitialized]