下面是一个例子:
#include <iostream>
struct Data
{
int x, y;
};
Data fill(Data& data)
{
data.x=3;
data.y=6;
return data;
}
int main()
{
Data d=fill(d);
std::cout << "x=" << d.x << ", y=" << d.y << "\n";
}
字符串
这里d
是从fill()
的返回值复制初始化的,但是fill()
在返回结果之前会写入d
本身。我担心的是,d
在初始化之前会被非平凡地使用,并且在某些情况下使用未初始化的变量会导致未定义的行为。
那么这段代码是有效的,还是它有未定义的行为?如果它是有效的,一旦Data
停止POD或在其他情况下,行为会变成未定义的吗?
3条答案
按热度按时间vecaoik11#
这看起来不像是有效的代码。它类似于问题中概述的情况:Is passing a C++ object into its own constructor legal?,尽管在这种情况下代码是有效的。机制并不相同,但基本的推理至少可以让我们开始。
我们从缺陷报告363开始,它问:
如果是的话,UDT的自初始化的语义是什么?
字符串
可以编译并打印:
型
决议草案是:
3.8[basic.life]第6段表明这里的引用是有效的。允许在完全初始化之前获取类对象的地址,并且允许将其作为引用参数的参数传递,只要引用可以直接绑定。[...]
因此,尽管
d
没有完全初始化,但我们可以将其作为引用传递。我们开始陷入麻烦的地方是在这里:
型
C++标准草案第
3.8
节(* 缺陷报告引用的同一节和段落 )说( 强调我的 *):类似地,在对象的生存期开始之前,但在对象将占用的存储空间已经分配之后,或者在对象的生存期结束之后,但在对象占用的存储空间被重用或释放之前,任何引用原始对象的glvalue都可以使用,但只能以有限的方式使用。对于正在构造或销毁的对象,请参见12.7。否则,这样的glvalue引用分配的存储(3.7.4.2),并且使用不依赖于其值的glvalue的属性是明确定义的。如果:
*glvalue用于访问非静态数据成员或调用对象的非静态成员函数,或
那么,access 是什么意思呢?缺陷报告1531对此进行了澄清,它将access定义为:
接入
读取或修改对象的值
所以
fill
* 访问 * 一个非静态数据成员,因此我们有未定义的行为。这也与第
12.7
节一致,该节说:[...]要形成指向对象obj的直接非静态成员的指针(或访问其值),obj的构造必须已经开始,其析构必须尚未完成,否则指针值的计算(或访问成员值)将导致未定义的行为。
既然你使用的是一个副本,你可以在
fill
中创建一个Data示例并初始化它,这样就避免了传递d
。正如T.C.所指出的,明确引用生命周期开始的细节是很重要的。来自第
3.8
节:一个对象的生命周期是对象的运行时属性。如果一个对象是类或聚合类型的,并且它或它的一个成员是由一个构造函数而不是平凡的默认构造函数初始化的,那么这个对象被称为具有非平凡的初始化。[注:平凡的复制/移动构造函数的初始化是非平凡的初始化。-结束注] T类型的对象的生命周期开始于:
初始化是不平凡的,因为我们是通过复制构造函数初始化的。
slhcrj9b2#
我没看到问题。如果未初始化的整数成员是有效的,因为你是为了写而访问的。* 阅读 * 它们会导致UB。
new9mtju3#
我认为这是有效的(疯狂,但有效)。
这将是法律的和逻辑上可以接受的:
字符串
事实上,这个形式是一样的:
型
就语言的逻辑结构而言,这两个版本是等效的。
所以这是法律的,逻辑上对语言是正确的。
然而,由于我们通常希望人们在创建变量时将其初始化为默认值(出于安全考虑),这是 * 糟糕的编程实践 *。
有趣的是,g++ -Wall编译这段代码时没有blurp。