初始化中的评估顺序
Evaluation order in initialization
在以下程序中:
#include <iostream>
struct I {
int i;
I(){i=2;}
I(int _i){i=_i;}
};
int a[3] = {a[2] = 1};
int aa[3][3] = {aa[2][2] = 1};
I A[3] = {A[2].i = 1};
I AA[3][3] = {AA[2][2].i = 1};
int main(int argc, char **argv) {
for (int b : a) std::cout << b << ' ';
std::cout << '\n';
for (auto &bb : aa) for (auto &b : bb) std::cout << b << ' ';
std::cout << '\n';
for (auto &B : A) std::cout << B.i << ' ';
std::cout << '\n';
for (auto &BB : AA) for (auto &B : BB) std::cout << B.i << ' ';
std::cout << '\n';
return 0;
}
输出为
1 0 0
1 0 0 0 0 0 0 0 1
1 2 2
1 2 2 2 2 2 2 2 2
来自 http://ideone.com/1ueWdK with clang3.7
但结果是:
0 0 1
1 0 0 0 0 0 0 0 1
1 2 2
1 2 2 2 2 2 2 2 2
在 http://rextester.com/l/cpp_online_compiler_clang 上也有 clang 3.7。
就我自己而言 ubuntu,gcc 6.2 在构造 int aa[3][3] = {aa[2][2] = 1}
.
上给出了一个内部编译器错误
我假设这是未定义的行为,但在标准中找不到明确的声明。
问题是:
是否在标准中定义了对初始化列表中的赋值(例如a[2] = 1
)和数组实际元素的初始化(例如a[2]
)的副作用的评估顺序?
明确说明是已定义还是未定义?还是因为没有明确定义而变成未定义?
或者由于评估顺序之外的其他原因,构造是否具有已定义或未定义的行为?
让我们从最简单的情况开始:
I A[3] = {A[2].i = 1};
I AA[3][3] = {AA[2][2].i = 1};
由于违反了 [basic.life],这两个都是 UB。您正在访问对象的生命周期开始之前的值。 I
没有平凡的默认构造函数,因此不能空洞地初始化。因此,对象的生命周期仅在构造函数完成后才开始。当您访问该数组的元素时,A
数组的元素尚未构造。
因此,您正在通过访问尚未构建的对象来调用 UB。
现在,其他两种情况更复杂:
int a[3] = {a[2] = 1};
int aa[3][3] = {aa[2][2] = 1};
请参阅,int
允许 "vacuous initialization",如 [basic.life]/1 所定义。已获取 a
和 aa
的存储空间。因此,int a[3]
是一个有效的 int
对象数组,即使聚合初始化尚未开始。所以访问对象甚至设置它的状态都不是 UB。
这里的操作顺序是固定的。即使是 C++17 之前的版本,初始化列表元素的初始化也会在聚合初始化被调用之前排序,如 [dcl.init.list]/4 中所述。未在此处的初始化列表中列出的聚合中的元素将被填充,就像 typename{}
构造一样。 int{}
表示对 int
进行值初始化,结果为 0.
因此,即使您设置了 a[2]
和 aa[2][2]
,它们也应该立即通过聚合初始化被覆盖。
因此,所有这些编译器都是错误的。答案应该是:
1 0 0
1 0 0 0 0 0 0 0 0
现在承认,这一切都是非常愚蠢的,你不应该这样做。但从纯语言的角度来看,这是定义明确的行为。
在以下程序中:
#include <iostream>
struct I {
int i;
I(){i=2;}
I(int _i){i=_i;}
};
int a[3] = {a[2] = 1};
int aa[3][3] = {aa[2][2] = 1};
I A[3] = {A[2].i = 1};
I AA[3][3] = {AA[2][2].i = 1};
int main(int argc, char **argv) {
for (int b : a) std::cout << b << ' ';
std::cout << '\n';
for (auto &bb : aa) for (auto &b : bb) std::cout << b << ' ';
std::cout << '\n';
for (auto &B : A) std::cout << B.i << ' ';
std::cout << '\n';
for (auto &BB : AA) for (auto &B : BB) std::cout << B.i << ' ';
std::cout << '\n';
return 0;
}
输出为
1 0 0
1 0 0 0 0 0 0 0 1
1 2 2
1 2 2 2 2 2 2 2 2
来自 http://ideone.com/1ueWdK with clang3.7
但结果是:
0 0 1
1 0 0 0 0 0 0 0 1
1 2 2
1 2 2 2 2 2 2 2 2
在 http://rextester.com/l/cpp_online_compiler_clang 上也有 clang 3.7。
就我自己而言 ubuntu,gcc 6.2 在构造 int aa[3][3] = {aa[2][2] = 1}
.
我假设这是未定义的行为,但在标准中找不到明确的声明。
问题是:
是否在标准中定义了对初始化列表中的赋值(例如a[2] = 1
)和数组实际元素的初始化(例如a[2]
)的副作用的评估顺序?
明确说明是已定义还是未定义?还是因为没有明确定义而变成未定义?
或者由于评估顺序之外的其他原因,构造是否具有已定义或未定义的行为?
让我们从最简单的情况开始:
I A[3] = {A[2].i = 1}; I AA[3][3] = {AA[2][2].i = 1};
由于违反了 [basic.life],这两个都是 UB。您正在访问对象的生命周期开始之前的值。 I
没有平凡的默认构造函数,因此不能空洞地初始化。因此,对象的生命周期仅在构造函数完成后才开始。当您访问该数组的元素时,A
数组的元素尚未构造。
因此,您正在通过访问尚未构建的对象来调用 UB。
现在,其他两种情况更复杂:
int a[3] = {a[2] = 1}; int aa[3][3] = {aa[2][2] = 1};
请参阅,int
允许 "vacuous initialization",如 [basic.life]/1 所定义。已获取 a
和 aa
的存储空间。因此,int a[3]
是一个有效的 int
对象数组,即使聚合初始化尚未开始。所以访问对象甚至设置它的状态都不是 UB。
这里的操作顺序是固定的。即使是 C++17 之前的版本,初始化列表元素的初始化也会在聚合初始化被调用之前排序,如 [dcl.init.list]/4 中所述。未在此处的初始化列表中列出的聚合中的元素将被填充,就像 typename{}
构造一样。 int{}
表示对 int
进行值初始化,结果为 0.
因此,即使您设置了 a[2]
和 aa[2][2]
,它们也应该立即通过聚合初始化被覆盖。
因此,所有这些编译器都是错误的。答案应该是:
1 0 0 1 0 0 0 0 0 0 0 0
现在承认,这一切都是非常愚蠢的,你不应该这样做。但从纯语言的角度来看,这是定义明确的行为。