Cla*_*diu 35 c++ undefined-behavior language-lawyer c++11 c++14
考虑以下:
struct mystruct
{
int i;
int j;
};
int main(int argc, char* argv[])
{
mystruct foo{45, foo.i};
std::cout << foo.i << ", " << foo.j << std::endl;
return 0;
}
Run Code Online (Sandbox Code Playgroud)
请注意foo.i在aggregate-initializer列表中的使用.
g++ 5.2.0 输出
45,45
这是明确定义的行为吗?是foo.i在这个聚集型初始化始终保证指存在创建结构的i元素(和&foo.i将指向内存地址,例如)?
如果我添加一个显式构造函数mystruct:
mystruct(int i, int j) : i(i), j(j) { }
Run Code Online (Sandbox Code Playgroud)
然后我收到以下警告:
main.cpp:15:20: warning: 'foo.a::i' is used uninitialized in this function [-Wuninitialized]
a foo{45, foo.i};
^
main.cpp:19:34: warning: 'foo.a::i' is used uninitialized in this function [-Wuninitialized]
cout << foo.i << ", " << foo.j << endl;
Run Code Online (Sandbox Code Playgroud)
代码编译,输出为:
45,0
显然,这有所不同,我假设这是未定义的行为.是吗?如果是这样,为什么这个和没有构造函数之间的区别?而且,如何使用用户定义的构造函数获取初始行为(如果它是定义良好的行为)?
Sha*_*our 14
你的第二种情况是未定义的行为,你不再使用聚合初始化,它仍然是列表初始化,但在这种情况下你有一个被调用的用户定义的构造函数.为了将第二个参数传递给构造函数,它需要进行求值,foo.i但它尚未初始化,因为您尚未进入构造函数,因此生成不确定的值并生成不确定的值是未定义的行为.
我们还有部分12.7构造和销毁[class.cdtor],其中说:
对于具有非平凡构造函数的对象,在构造函数开始执行之前引用该对象的任何非静态成员或基类会导致未定义的行为[...]
所以我没有看到让你的第二个例子像第一个例子一样工作的方法,假设第一个例子确实有效.
你的第一个案例似乎应该很好地定义,但我在标准草案中找不到一个似乎明确的参考.也许它是缺陷,但否则它将是未定义的行为,因为标准没有定义行为.该标准告诉我们的是,初始化器按顺序进行评估,副作用按顺序排序,来自8.5.4 [dcl.init.list]部分:
在braced-init-list的initializer-list中,initializer-clauses(包括pack扩展(14.5.3)产生的任何结果)按照它们出现的顺序进行评估.也就是说,与给定初始化子句相关联的每个值计算和副作用在每个值计算和副作用之前与在初始化列表的逗号分隔列表中跟随它之后的任何初始化子句相关联.[...]
但我们没有明确的文字说明在评估每个元素后会对成员进行初始化.
MSalters认为该部分1.9说:
访问由volatile glvalue(3.10)指定的对象,修改对象,调用库I/O函数或调用执行任何这些操作的函数都是副作用,这些都是执行环境状态的变化.[...]
结合:
[...]非常值计算和与给定初始化子句相关的副作用在每个值计算和与其后的任何初始化子句相关联的副作用之前被排序[...]
足以保证在评估初始化程序列表的元素时初始化聚合的每个成员.虽然这在C++ 11之前不适用,因为初始化列表的评估顺序未指定.
作为参考,如果标准没有强制要求,则行为未定义来自1.3.24定义未定义行为的部分:
本国际标准没有要求的行为[注:当本国际标准忽略任何明确的行为定义或[...]时,可能会出现未定义的行为.
更新
Johannes Schaub指出了缺陷报告1343:非类初始化和std讨论线程的排序是否与相应的initializer-clause相关联的聚合成员复制初始化?并且是与相应的initializer子句相关联的聚合成员的复制初始化?这些都是相关的.
他们基本上指出第一个案例目前尚未明确,我将引用理查德史密斯:
所以唯一的问题是,初始化si的副作用是"与"完整表达式"5"的评估"相关联"吗?我认为唯一合理的假设是:如果5正在初始化类类型的成员,那么构造函数调用显然是[intro.execution] p10中定义的完整表达式的一部分,所以很自然地假设标量类型也是如此.
但是,我不认为标准实际上明确地说明了这一点.
因此,虽然如某些地方所示,当前的实现看起来像我们期望的那样,但在正式澄清或实现提供保证之前依赖它似乎是不明智的.
使用指定初始化建议:P0329对于第一种情况,此问题的答案会发生变化.它包含以下部分:
添加一个新段落到11.6.1 [dcl.init.aggr]:
聚合元素的初始化按元素顺序进行评估.也就是说,之前对与给定元素相关的所有值计算和副作用进行排序
我们可以看到这反映在最新的标准草案中
Nat*_*ica 12
来自[dcl.init.aggr] 8.5.1(2)
当初始化程序列表初始化聚合时,如8.5.4中所述,初始化程序列表的元素将作为聚合成员的初始化程序,增加下标或成员顺序.每个成员都是从相应的initializer子句复制初始化的.
强调我的
和
在braced-init-list的initializer-list中,initializer-clauses(包括pack扩展(14.5.3)产生的任何结果)按照它们出现的顺序进行评估.也就是说,与给定初始化子句相关联的每个值计算和副作用在每个值计算和副作用之前与在初始化列表的逗号分隔列表中跟随它之后的任何初始化子句相关联.
让我相信类的每个成员将按照它们在初始化列表中声明的顺序进行初始化,并且foo.i在我们评估它之前初始化它以初始化j它应该是定义的行为.
这也是[intro.execution] 1.9(12)的备份
访问由volatile glvalue(3.10)指定的对象,修改对象,调用库I/O函数或调用执行任何这些操作的函数都是副作用,这些都是执行环境状态的变化.
强调我的
在您的第二个示例中,我们不使用聚合初始化而是列表初始化.[dcl.init.list] 8.5.4(3)有
类型T的对象或引用的列表初始化定义如下:
[...]
- 否则,如果T是类类型,则考虑构造函数.枚举适用的构造函数,并通过重载决策(13.3,13.3.1.7)选择最佳构造函数.
所以现在我们会调用你的构造函数.当调用构造函数foo.i尚未初始化时,我们正在复制未初始化的变量,这是一个未定义的行为.
| 归档时间: |
|
| 查看次数: |
811 次 |
| 最近记录: |