所以我们在这里看到的是 C++ 标准草案的 8.5.1 部分中涵盖的聚合初始化,它说:
聚合是一个数组或一个类 [...]
和:
当一个聚合被初始化列表初始化时,指定
在 8.5.4 中,初始化列表的元素被视为
聚合成员的初始化器,增加下标
或会员订单。每个成员都从
相应的初始化子句 [...]
虽然初始化聚合的每个成员的副作用应该在下一个之前排序似乎是合理的,因为初始化列表中的每个元素都是一个完整的表达式。该标准实际上并不能保证这一点,我们可以从defect report 1343 看到这一点:
目前的措辞并不表示非类对象的初始化是一个完整的表达式,但大概应该这样做。
还有注意事项:
聚合初始化也可能涉及多个完整表达式,因此上面对“非类对象的初始化”的限制是不正确的。
我们可以从相关的std-discussion topicRichard Smith 中看到:
[intro.execution]p10: "一个完整的表达式是一个不是
另一个表达式的子表达式。 [...] 如果语言结构
被定义为产生一个函数的隐式调用,使用
语言结构被认为是一种表达方式
这个定义。”
因为花括号初始化列表不是表达式,在这种情况下它
不会导致函数调用,5 和 s.i 是分开的
完整的表达式。那么:
[intro.execution]p14: "每个值的计算和副作用
与完整表达式相关联在每个值之前排序
与下一个完整表达式相关的计算和副作用
待评估。”
所以唯一的问题是,初始化 s.i 的副作用是什么?
“与”完整表达式“5”的评估“相关”?我想
唯一合理的假设是:如果 5 正在初始化一个
类类型的成员,构造函数调用显然是
[intro.execution]p10 中定义的完整表达式,所以它
很自然地假设标量类型也是如此。
但是,我认为标准实际上并没有明确说明这一点
任何地方。
因此,目前标准并未对此进行规定,也不能依赖它,尽管如果实现没有按照您期望的方式处理它,我会感到惊讶。
对于像这样的简单情况,类似这样的东西似乎是更好的选择:
constexpr int value = 13 ;
const int foo[2] = {value, value+42};
C++17 的变化
proposal P0507R0: Core Issue 1343: Sequencing of non-class initialization 澄清了here 提出的完整表达式点,但没有回答有关初始化的副作用是否包含在完整表达式的评估中的问题。所以这并没有改变,这是未指定的。
这个问题的相关改动在[intro.execution]:
组成表达式定义如下:
(9.1) — 表达式的组成表达式就是那个表达式。
(9.2) — 括号初始化列表或(可能带括号的)表达式列表的组成表达式是
各个列表元素的组成表达式。
(9.3) — 形式为 = initializer-clause 的大括号或等式初始化器的组成表达式是
初始化子句的组成表达式。
[ 例子:
struct A { int x; };
struct B { int y; struct A a; };
B b = { 5, { 1+1 } };
用于初始化b的初始化器的组成表达式是5和1+1。 ——结束示例]
和[intro.execution]p12:
一个完整的表达式是
(12.1) — 未计算的操作数(第 8 条),
(12.2) — 一个常量表达式 (8.20),
(12.3) — 一个 init-declarator (Clause 11) 或一个 mem-initializer (15.6.2),包括
初始化器,
(12.4) — 在对象的生命周期结束时生成的析构函数调用,而不是临时对象
对象 (15.2),或
(12.5) — 一个表达式不是另一个表达式的子表达式,也不是其他表达式的一部分
完整的表达。
所以在这种情况下13 和foo[0] + 42 都是组成表达式,它们是完整表达式的一部分。这与analysis here 有所不同,后者假设它们每个都是自己的完整表达式。
C++20 的变化
Designated Initialization proposal: P0329 包含以下添加内容,似乎可以很好地定义:
在 11.6.1 [dcl.init.aggr] 中添加一个新段落:
聚合元素的初始化按元素顺序进行评估。那是,
与给定元素相关的所有值计算和副作用都按顺序排在其后的任何元素之前。
我们可以看到这反映在latest draft standard。