Относительно поведения Struct Constructor и Destructor - C++
Я не понимаю, почему вывод этой программы следующий. Почему нет ошибки компиляции? Я думал, что при попытке построить B компилятор не найдет функцию с именем foo() и сообщит об ошибке.
#include <iostream>
using namespace std;
struct A{
int a;
A(int i=0) : a(i) { cout << "A" << endl; }
~A() { cout << "Bye A" << endl; }
int foo() { return a; }
};
struct B{
int b;
B(int i=0) : b(i) { cout << "B" << endl; }
~B() { cout << "Bye B" << endl; }
int bar() { return b; }
};
struct C : B, A {
C(int i=0) : B(foo()), A(i) {}
};
int main() {
cout << C(10).bar() << endl;
return 0;
}
Выход:
B
A
0
Bye A
Bye B
В общем, я хотел бы знать, когда существует множественное наследование, в каком порядке создаются и инициализируются родительские структуры? Можно ли ожидать такого же поведения в классах?
Любое объяснение порядка вызовов конструктора и деструктора очень ценится.
Примечание: это не домашняя работа. И я исследовал подобные темы, но ничего не пришло по этому вопросу.
2 ответа
Неопределенное поведение
Вы вызываете неопределенное поведение, вызывая foo
до того, как объект полностью инициализирован. Цитата из 12.6.2 в стандарте C++:
Функции-члены (включая виртуальные функции-члены, 10.3) могут быть вызваны для строящегося объекта. Точно так же строящийся объект может быть операндом
typeid
оператор (5.2.8) илиdynamic_cast
(5.2.7). Однако, если эти операции выполняются в инициализаторе ctor (или в функции, вызываемой прямо или косвенно из инициализатора ctor) до завершения всех инициализаторов mem для базовых классов, результат операции не определен. [ Пример:
class A {
public:
A(int);
};
class B : public A {
int j;
public:
int f();
B() : A(f()), // undefined: calls member function
// but base A not yet initialized
j(f()) { } // well-defined: bases are all initialized
};
class C {
public:
C(int);
};
class D : public B, C {
int i;
public:
D() : C(f()), // undefined: calls member function
// but base C not yet initialized
i(f()) { } // well-defined: bases are all initialized
};
- конец примера]
Другими словами, это будет нормально в соответствии со стандартом:
C(int i=0) : B(), A(i) {
B::b = foo();
}
И это напечатает 10
вместо 0
что вы получили (что могло бы быть что-нибудь еще, так как это было неопределенное поведение).
Порядок инициализации
Если оставить в стороне этот вопрос неопределенного поведения и ответить на ваш вопрос, порядок, в котором происходит инициализация, четко определен:
В не делегирующем конструкторе инициализация происходит в следующем порядке:
- Во-первых, и только для конструктора самого производного класса (1.8), виртуальные базовые классы инициализируются в том порядке, в котором они отображаются при обходе слева направо по глубине ориентированного ациклического графа базовых классов, где "слева" -to-right "- порядок появления базовых классов в списке базовых спецификаторов производного класса.
- Затем прямые базовые классы инициализируются в порядке объявления по мере их появления в списке базовых спецификаторов (независимо от порядка mem-initializer).
- Затем элементы нестатических данных инициализируются в том порядке, в котором они были объявлены в определении класса (опять же, независимо от порядка инициализаторов mem).
- Наконец, составной оператор тела конструктора выполняется.
[Примечание: Порядок декларации должен гарантировать уничтожение базовых и дочерних подобъектов в обратном порядке инициализации. - конец примечания]
Итак, в вашем коде порядок инициализации: B
(B::b
), A
(A::a
), C
().
Как отмечено в комментариях ниже, хотя, изменение этого порядка инициализации (например, с помощью struct C : A, B
вместо struct C : B, A
) однако не избавится от неопределенного поведения. призвание A::foo
перед B
инициализированная часть остается неопределенной, даже если A
часть инициализирована.
Это просто еще один случай неопределенного поведения. Например, моя система дает следующие результаты.
B
A
-858993460
Bye A
Bye B
Попробуйте это живое демо, которое дает еще один отличный результат (C(10).bar()
произведено 32764).
foo()
может быть вызван в этом контексте, но он будет вызван раньше A
конструктор. Это означает a
инициализируется, что приводит к чтению неинициализированной переменной, что приводит к неопределенному поведению. Это похоже на доступ к члену до его инициализации. Рассмотрим следующий пример. a
инициализируется в b
значит, тогда b
инициализируется. Проблема очевидна, b
неинициализирован в точке, где он читается для инициализации a
,
struct foo
{
foo(int x) : a(b), b(x) {}
int a;
int b;
};
int main()
{
foo bar(10);
}
Я хотел посмотреть, что происходит на самом деле. Поскольку у меня есть результаты, вот они для вас.
#include <iostream>
using namespace std;
/*** Structures ***/
struct A{
int a;
#warning a integer declared in struct A
A(int i=0) : a(i)
#warning A constuctor declared
#warning A constructor creates variable i, sets it to 0
#warning then assigns i to a
{
cout << "inside constructor A" << endl;
cout << "i = " << i << endl;
cout << "a = " << a << endl;
cout << "leaving constructor A" << endl;
}
#warning A constructor definition provided
~A()
#warning A destructor declared
{
cout << "inside destructor A" << endl;
cout << "leaving destructor A" << endl;
}
#warning A destructor definition provided
int foo()
#warning foo function declared in struct A
{
cout << "inside foo, inside A" << endl;
cout << "foo will return a = " << a << " then leave foo" << endl;
return a;
}
#warning foo function defined in struct A
};
struct B{
int b;
#warning b integer declared in struct B
B(int i=0) : b(i)
#warning B constructor declared
#warning B creates int i and initializes it to 0
#warning b is assigned the value of i
{
cout << "inside constructor B" << endl;
cout << "i = " << i << endl;
cout << "b = " << b << endl;
cout << "leaving constructor B" << endl;
}
#warning B constructor defined
~B()
#warning B destructor declared
{
cout << "inside destructor B" << endl;
cout << "leaving destructor B" << endl;
}
#warning B destructor defined
int bar()
#warning bar function declared in struct B
{
cout << "inside bar, inside B" << endl;
cout << "bar will return b = " << b << " then leave bar" << endl;
return b;
}
#warning bar function defined in struct B
};
struct C : B, A
#warning C structure declared derived from B and A
{
C(int i=0) : B(foo()), A(i)
#warning C constructor declared
#warning C constructor creates int i and assigns value 0
#warning C constructor instantiates B and calls foo from A to assign value?
#warning C constructor instantiates A by assigning i to it
{
cout << "inside constructor C" << endl;
cout << "i = " << i << endl;
cout << "leaving constructor C" << endl;
}
#warning C constructor defined with no implementation
};
int main() {
cout << "command is: print the value of C(10).bar()" << endl;
cout << C(10).bar() << endl;
#warning initialize C with a value of 10
#warning then call the bar function extended from B
#warning declare struct C blah, initialized with C(12)
cout << endl << "creating struct blah with definition C(12)" << endl;
struct C blah = C(12);
cout << "calling blah.foo" << endl;
cout << blah.foo() << endl;
cout << "calling blah.bar" << endl;
cout << blah.bar() << endl;
#warning printing and then returning 0
cout << endl << "Some random output before returning 0" << endl;
return 0;
}
Выдает следующие результаты компиляции (немного почистил):
>make
test.cpp:7:2: warning: #warning a integer declared in struct A [-Wcpp]
test.cpp:10:2: warning: #warning A constuctor declared [-Wcpp]
test.cpp:11:2: warning: #warning A constructor creates variable i, sets it to 0 [-Wcpp]
test.cpp:12:2: warning: #warning then assigns i to a [-Wcpp]
test.cpp:19:2: warning: #warning A constructor definition provided [-Wcpp]
test.cpp:21:2: warning: #warning A destructor declared [-Wcpp]
test.cpp:26:2: warning: #warning A destructor definition provided [-Wcpp]
test.cpp:28:2: warning: #warning foo function declared in struct A [-Wcpp]
test.cpp:34:2: warning: #warning foo function defined in struct A [-Wcpp]
test.cpp:39:2: warning: #warning b integer declared in struct B [-Wcpp]
test.cpp:41:2: warning: #warning B constructor declared [-Wcpp]
test.cpp:42:2: warning: #warning B creates int i and initializes it to 0 [-Wcpp]
test.cpp:43:2: warning: #warning b is assigned the value of i [-Wcpp]
test.cpp:50:2: warning: #warning B constructor defined [-Wcpp]
test.cpp:52:2: warning: #warning B destructor declared [-Wcpp]
test.cpp:57:2: warning: #warning B destructor defined [-Wcpp]
test.cpp:59:2: warning: #warning bar function declared in struct B [-Wcpp]
test.cpp:65:2: warning: #warning bar function defined in struct B [-Wcpp]
test.cpp:70:2: warning: #warning C structure declared derived from B and A [-Wcpp]
test.cpp:73:2: warning: #warning C constructor declared [-Wcpp]
test.cpp:74:2: warning: #warning C constructor creates int i and assigns value 0 [-Wcpp]
test.cpp:75:2: warning: #warning C constructor instantiates B and calls foo from A to assign value? [-Wcpp]
test.cpp:76:2: warning: #warning C constructor instantiates A by assigning i to it [-Wcpp]
test.cpp:82:2: warning: #warning C constructor defined with no implementation [-Wcpp]
test.cpp:88:2: warning: #warning initialize C with a value of 10 [-Wcpp]
test.cpp:89:2: warning: #warning then call the bar function extended from B [-Wcpp]
test.cpp:91:2: warning: #warning declare struct C blah, initialized with C(12) [-Wcpp]
test.cpp:98:2: warning: #warning printing and then returning 0 [-Wcpp]
И дает следующий результат:
>test
command is: print the value of C(10).bar()
inside foo, inside A
foo will return a = 4201198 then leave foo
inside constructor B
i = 4201198
b = 4201198
leaving constructor B
inside constructor A
i = 10
a = 10
leaving constructor A
inside constructor C
i = 10
leaving constructor C
inside bar, inside B
bar will return b = 4201198 then leave bar
4201198
inside destructor A
leaving destructor A
inside destructor B
leaving destructor B
creating struct blah with definition C(12)
inside foo, inside A
foo will return a = 4201104 then leave foo
inside constructor B
i = 4201104
b = 4201104
leaving constructor B
inside constructor A
i = 12
a = 12
leaving constructor A
inside constructor C
i = 12
leaving constructor C
calling blah.foo
inside foo, inside A
foo will return a = 12 then leave foo
12
calling blah.bar
inside bar, inside B
bar will return b = 4201104 then leave bar
4201104
Some random output before returning 0
inside destructor A
leaving destructor A
inside destructor B
leaving destructor B