Почему он работает, когда он нарушает правило порядка списка инициализации

Почему этот код работает? Я ожидал, что это провалится из-за нарушения одного из основных правил С++:

#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-конструкторе B дается ссылка на объект A, если этот объект еще не создан.

Я что-то упустил? Правило порядка инициализации совпадает с порядком полей, которое не применяется для ссылок?

EDIT: Меня еще больше удивляет, что я могу добавить вызов "a.doSth();" внутри конструктора B, и это также сработает. Зачем? В этот момент объект A не должен существовать!

Ответы

Ответ 1

Ваш код в порядке, если конструктор B не использует эту ссылку для чего-либо, кроме привязки его члена. Хранилище для a уже было выделено при запуске c'tor C, и, как Sneftel, он отображается в области, Таким образом, вы можете воспользоваться ссылкой, поскольку [basic.life]/7 явно разрешает:

Аналогично, до начала жизни объекта, но после хранилище, которое будет занимать объект, или, после срок службы объекта закончился и перед хранилищем, которое объект, занятый, повторно используется или освобождается, любое значение gl, которое ссылается на оригинальный объект может использоваться, но только ограниченным образом. Для объекта под строительство или уничтожение, см. [class.cdtor]. В противном случае glvalue относится к выделенному хранилищу ([basic.stc.dynamic.deallocation]) и используя свойства glvalue, которые не зависят от его значения, четко определены. Программа имеет поведение undefined, если:

  • glvalue используется для доступа к объекту или
  • glvalue используется для вызова нестатической функции-члена объекта или
  • glvalue привязан к ссылке на виртуальный базовый класс ([dcl.init.ref]) или
  • glvalue используется как операнд dynamic_cast или как операнд typeid.

Относительно вашего редактирования:

Что еще меня удивляет, так это то, что я могу добавить вызов "a.doSth();" внутри конструктора B, и это также сработает. Зачем? В этот момент объект A не должен существовать!

Undefined поведение undefined. Вторая пуля в параграфе I, о которой я говорил, в значительной степени говорит об этом. Компилятор может быть достаточно умен, чтобы поймать его, но это не обязательно.

Ответ 2

В фрагменте кода, когда C создается, a не был инициализирован, но он уже находится в области видимости, поэтому компилятор не обязан выдавать диагностику. Его значение undefined.

Код хорош в том смысле, что B::a является правильным псевдонимом C::a. Срок службы поддержки хранилища C::a уже начат по истечении времени B::B().

В отношении вашего редактирования: Хотя срок хранения C::a уже начался, a.doSth() из B::B() будет абсолютно приводить к поведению undefined (google, чтобы узнать, почему что-то может быть UB и все еще "работает" ).

Ответ 3

Это работает, потому что вы не получаете доступ к неинициализированному полю C::a во время инициализации C::b. Вызывая C() : b(a), вы привязываете ссылку на a для конструктора B(A& a). Если вы измените свой код, чтобы каким-то образом использовать неинициализированное значение, это будет поведение undefined:

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

Undefined Поведение означает, что все возможно, в том числе, кажется, работает нормально. Это не значит, что на следующей неделе это будет нормально работать или даже в следующий раз, когда вы запустите его - вы можете получить демоны, летящие из вашего носа.

Что, вероятно, происходит при вызове a.doSth(), заключается в том, что компилятор преобразует вызов в статический a::doSth(); поскольку он не является виртуальной функцией, ему не нужно обращаться к объекту для совершения вызова. Сама функция не использует никаких переменных-членов или функций, поэтому не генерируются недействительные обращения. Он работает, хотя он не гарантированно работает.

Ответ 5

Он не "работает" в том смысле, что объект a, используемый для инициализации, еще не назвал свой конструктор (который показывает ваш журнал) - это означает, что init b мог или не мог сбой в зависимости от того, что делает a.

Компилятор не мешает этому, но я предполагаю, что это должно произойти. Во всяком случае, я не думаю, что это UB, если вы на самом деле не пытаетесь использовать унифицированный объект; просто сохранить ссылку должно быть хорошо.

Ответ 6

Это работает, потому что B инициализируется ссылкой, и эта ссылка уже существует, поэтому ее можно использовать для инициализации с ней.

Если вы попытаетесь передать a по значению в ctor B, тогда компилятор будет жаловаться:

предупреждение: поле 'a' не инициализируется при использовании здесь       [-Wuninitialized]