Почему он работает, когда он нарушает правило порядка списка инициализации
Почему этот код работает? Я ожидал, что это провалится из-за нарушения одного из основных правил С++:
#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]