让我们假设我们有一个类B
,它有一个member
,默认情况下初始化为42
。此类知道如何打印其member
的值(它在构造函数中这样做):
struct B
{
B() : member(42) { printMember(); }
void printMember() const { std::cout << "value: " << member << std::endl; }
int member;
};
然后,我们添加一个类A
,该类接收对B
的const引用,并要求B
打印其值:
struct A
{
A(const B& b) { b.printMember(); }
};
最后,我们添加另一个类Aggregate
,该类将A
和B
聚合在一起。棘手的部分是类型{{1}的对象a
在类型A
的对象b
之前被声明,但是随后B
使用(尚未生效? )对a
的引用:
b
考虑创建struct Aggregate
{
A a;
B b;
Aggregate() : a(b) { }
};
的输出(我已经为Aggregate
和A
的构造函数和析构函数添加了一些日志记录)(Try it online!):
B
我是否可以正确地假设引用a c'tor
value: 0
b c'tor
value: 42
b d'tor
a d'tor
实例(尚未有效)初始化a
是无效的,因此这是未定义的行为吗?
我知道初始化顺序。这就是让我挣扎的原因。我知道 b
尚未构建,但我也认为知道b
的未来地址甚至可以在{{ 1}}被构造。因此,我认为可能存在一些我不知道的规则 ,该规则允许编译器在构造b
或类似的东西之前默认初始化b
的成员。 (如果第一个打印出的值看起来像是随机的而不是b
(默认值b
,那会更明显)。
This answer帮助我了解到我需要区别对待
答案 0 :(得分:23)
是的,它是UB是正确的,但是出于不同的原因,而不是仅仅存储对尚未构造的对象的引用。
类成员的构建按其在类中的出现顺序进行。尽管B
的地址不会改变,并且从技术上讲您是can store a reference to it,如@StoryTeller所指出的那样,使用尚未构建的b.printMember()
在构造函数中调用b
绝对是UB。
答案 1 :(得分:1)
类成员的初始化顺序如下。
根据CPP标准(N4713),相关部分突出显示:
15.6.2初始化基和成员[class.base.init] ...
13在非委托构造函数中,初始化按以下顺序进行:
(13.1)—首先,并且仅对于最派生类(6.6.2)的构造函数,将虚拟基类按照它们在基的有向无环图的深度优先从左到右遍历时出现的顺序进行初始化类,其中“从左到右”是基类在派生类base-specifier-list中的出现顺序。
(13.2)—然后,直接基类按照它们出现在base-specifier-list中的声明顺序进行初始化(与mem-initializer的顺序无关)。
(13.3)— 然后,非静态数据成员按照它们在类定义中声明的顺序进行初始化(同样,无论mem-initializer的顺序如何)。
(13.4)—最后,执行构造函数主体的复合语句。
[注意:声明顺序是强制执行的,以确保以相反的初始化顺序销毁基础和成员子对象。 —尾注]