悬空引用和未定义的行为

Dangling references and undefined behavior

提问人:Luchian Grigore 提问时间:2/6/2013 更新时间:2/6/2013 访问量:1352

问:

假设有一个悬空的引用。只是写是未定义的行为吗x

&x;

甚至

x;

?

C++ 参考手册 undefined-behavior language-lawyer

评论

0赞 John Dvorak 2/6/2013
我最好的猜测是它不是UD。 绝对是。*x;
2赞 Luchian Grigore 2/6/2013
x是一个参考,所以......不是很合法。*x
1赞 Sev08 2/6/2013
关于对 stackoverflow.com/a/14730198/1601207 的评论
2赞 davmac 2/6/2013
扬·德沃夏克:你怎么知道&x很好?特别是,如果引用对象的类型是重载 operator& 的类,则肯定是未定义的行为;标准中没有任何内容让我认为它是定义的,即使事实并非如此。
1赞 Luchian Grigore 2/6/2013
@davmac很好的观点。

答:

4赞 Joseph Mansfield 2/6/2013 #1

使用无效对象(引用、指针等)未定义行为的原因在于左值到右值的转换 (§4.1):

如果 glvalue 引用的对象不是 T 类型的对象,也不是派生自 T 的类型的对象,或者如果该对象未初始化,则需要此转换的程序具有未定义的行为。

假设我们没有重载,一元运算符将左值作为其操作数,因此不会发生转换。只有一个标识符,就像 中一样,也不需要转换。只有当引用在期望该操作数为右值的表达式中用作操作数时,才会获得未定义的行为 - 大多数运算符都是这种情况。关键是,执行实际上并不需要访问 的值。左值到右值的转换发生在那些需要访问其值的运算符上。operator&&x;&xx

我相信你的代码定义得很好。

当重载时,表达式将转换为函数调用,并且不遵守内置运算符的规则,而是遵循函数调用的规则。对于 ,函数调用的转换结果为 或 。在第一种情况下,当使用类成员访问运算符时,将发生左值到右值的转换。在第二种情况下,将 的参数用 (如 中 ) 进行复制初始化,其行为取决于参数的类型。例如,如果参数是左值引用,则不存在未定义的行为,因为不会发生左值到右值的转换。operator&&x&xx.operator&()operator&(x)xoperator&xT arg = x

因此,如果 的类型重载,则代码可能定义良好,也可能定义不明确,具体取决于函数的调用。operator&xoperator&

你可能会争辩说,一元运算符依赖于至少存在一些有效的存储区域,而你拥有的地址是:&

否则,如果表达式的类型为 ,则结果的类型为“指针”,并且是作为指定对象地址的 prvalueTT

对象被定义为存储区域。在引用的对象被销毁后,该存储区域将不再存在。

我更愿意相信,只有当实际访问无效对象时,它才会导致未定义的行为。引用仍然认为它指的是某个对象,即使它不存在,它也可以愉快地给出它的地址。然而,这似乎是标准中一个未明确说明的部分。


旁白

作为未定义行为的示例,请考虑 。现在,我们遇到了标准中另一个未指定的部分。未指定操作数的值类别。通常从 §5/8 中推断,如果未指定,则它需要 prvalue:x + x+

每当 glvalue 表达式显示为需要该操作数的 prvalue 的运算符的操作数时,将应用左值到右值 (4.1)、数组到指针 (4.2) 或函数到指针 (4.3) 标准转换以将表达式转换为 prvalue。

现在因为是左值,所以需要左值到右值的转换,我们得到未定义的行为。这是有道理的,因为加法需要访问 的值,以便它可以计算出结果。xx

评论

2赞 davmac 2/6/2013
但是,&-运算符可以重载。
0赞 Luchian Grigore 2/6/2013
答案的第二部分(您刚刚添加的)无关紧要。
0赞 Joseph Mansfield 2/6/2013
@davmac我希望这是这个问题的一个安全假设!
2赞 Potatoswatter 2/6/2013
左值是引用对象的表达式,其结果是对象的地址。但在这种情况下,没有对象。我不太确定标准到底说了什么。&
1赞 M.M 10/21/2017
在 C++ 中,对象存储是两个不同的概念。对象必须存在于存储中,但存储中可以没有对象。对象可以在不释放存储的情况下被销毁(这是如何实现的,例如,它获取存储并根据需要在存储中创建/移动/销毁对象)std::vector
4赞 Potatoswatter 2/6/2013 #2

假设使用有效对象进行初始化,然后销毁该对象,则 §3.8/6 适用:x

同样,在对象的生存期开始之前,但在分配了该对象将占用的存储之后,或者在对象的生存期结束后,在重用或释放对象占用的存储之前,可以使用引用原始对象的任何 glvalue,但只能以有限的方式使用。对于正在建造或破坏的物体,见12.7。否则,此类 glvalue 是指已分配的存储 (3.7.4.2),并且使用不依赖于其值的 glvalue 属性是明确定义的。如果出现以下情况,程序具有未定义的行为:

— 左值到右值的转换 (4.1) 应用于这样的 glvalue,

— glvalue 用于访问非静态数据成员或调用 对象,或者

— GLvalue 绑定到对虚拟基类 (8.5.3) 的引用,或者

— glvalue 用作 dynamic_cast (5.2.7) 的操作数或 typeid 的操作数。

因此,简单地获取地址是明确定义的,并且(参考相邻的段落)甚至可以有效地用于创建一个新对象来代替旧对象。

至于取地址而只是写,那真的完全没有作用,它是 的适当子表达。所以也没关系。x&x

评论

3赞 Angew is no longer proud of SO 2/6/2013
这意味着这仅适用于“在对象占用的存储被重复使用或释放之前”。
1赞 Potatoswatter 2/6/2013
@Angew是的。假设我可以进一步追溯,但我的一般直觉是,这种事情是诱惑命运。
0赞 M.M 10/21/2017
这个答案涵盖了存储仍然存在的情况,但是存储不存在的情况呢?(例如,函数返回对局部变量的引用 - 当函数返回时,自动变量会释放存储空间)
0赞 Potatoswatter 10/21/2017
@M.M:据我所知,这需要内存模型规范,C++仍然缺乏。UB在这里其他答案的“裂缝之间”的推理仍然成立。
4赞 Angew is no longer proud of SO 2/6/2013 #3

首先,非常有趣的问题。

我会说这是未定义的行为,假设“悬空引用”意味着“引用对象的生命周期已结束,并且对象占用的存储已被重用或释放”。我的推理基于以下标准裁决:

3.8 §3:

本国际标准中归因于对象的属性仅适用于给定对象 在其一生中。[ 注意:特别是,在对象的生存期开始之前和结束之后 对象的使用有很大的限制,如下所述......]

所有“如下所述”的情况均指

在对象的生存期开始之前,但在对象将占用的存储之后 已分配38 或者,在对象的生存期结束后,在对象占用的存储之前 重复使用或发布

1.3.24: 未定义的行为

本国际标准不设要求的行为 [ 注意:当本国际标准省略任何明确的定义时,可能会出现未定义的行为 行为或程序使用错误构造或错误数据时。...]

我对上述引文应用以下思路:

  1. 如果标准没有描述某种情况的行为,则行为是未定义的。
  2. 该标准仅描述了对象在其生命周期内的行为,以及接近其生命周期开始/结束的一些特殊情况。这些都不适用于我们的悬空参考。
  3. 因此,以任何方式使用丹灵引用都不具有标准规定的行为,因此该行为是未定义的。