悬空引用和未定义行为

14
假设有一个悬空引用 x。只写下面这行代码,是否属于未定义行为?
&x;

甚至更多
x;

?


2
x是一个引用,所以*x...并不是真正合法的。 - Luchian Grigore
Jan Dvorak:问题说的是悬空引用,而不是悬空指针。 - davmac
1
关于 http://stackoverflow.com/a/14730198/1601207 上的评论。 - Sev08
2
Jan Dvorak:你怎么确定 &x 是好的?特别是如果所引用对象的类型是重载 operator& 的类,那么它肯定是未定义行为;即使不是这种情况,标准中也没有任何东西让我认为它是被定义的。 - davmac
1
@davmac 很好的观点。 - Luchian Grigore
显示剩余11条评论
3个回答

5
使用无效对象(引用、指针等)的行为未定义的原因是左值到右值转换(§4.1):
如果glvalue所引用的对象不是类型T的对象,也不是派生自类型T的对象,或者该对象未初始化,则需要进行此转换的程序具有未定义行为。
假设我们没有重载operator&,则一元运算符&以其操作数作为左值,因此不会发生转换。只有一个标识符,如x;也不需要转换。只有当引用作为期望该操作数为右值的表达式的操作数时,才会得到未定义的行为——这是大多数运算符的情况。关键是,执行&x实际上并不需要访问x的值。仅在需要访问其值的运算符中才会发生左值到右值的转换。
我相信您的代码是良好定义的。
当已经重载operator&时,表达式&x会被转换为函数调用,并且不遵循内置运算符的规则,而是遵循函数调用的规则。对于&x,转换为函数调用将导致x.operator&()或operator&(x)。在第一种情况下,在使用类成员访问运算符时将对x进行左值到右值的转换。在第二种情况下,operator&的参数将使用x进行复制初始化(如T arg = x),其行为取决于参数的类型。例如,在参数为左值引用的情况下,不存在未定义的行为,因为不会进行左值到右值的转换。
因此,如果operator&已重载为x的类型,则代码可能是定义良好的,也可能是未定义的,这取决于调用operator&函数的方式。
您可以认为一元运算符&依赖于至少有一些有效的存储区域,您拥有该地址:
否则,如果表达式的类型为T,则结果具有类型“指向T的指针”,并且是指定对象的地址的prvalue 而对象被定义为存储区域。在所引用的对象被销毁后,该存储区域将不再存在。
我更倾向于相信,仅当实际访问无效对象时才会导致未定义的行为。引用仍然认为它正在引用某个对象,即使该对象不存在,它也可以愉快地提供其地址。但是,这似乎是标准中未明确规定的部分。

旁注

作为未定义行为的一个例子,考虑 x + x。现在我们遇到了标准中另一个不明确的部分。运算符 + 的操作数的值类别没有指定。通常可以从 §5/8 推断出,如果没有指定,则期望 prvalue:

每当 glvalue 表达式出现为一个运算符的操作数,该运算符期望该操作数为 prvalue 时,将应用 lvalue-to-rvalue(4.1)、array-to-pointer(4.2)或 function-to-pointer(4.3)标准转换将表达式转换为 prvalue。

现在因为 x 是 lvalue,所以需要进行 lvalue-to-rvalue 转换,这将导致未定义的行为。这是有道理的,因为加法需要访问 x 的值才能计算出结果。

2
&-运算符可以被重载。 - davmac
答案的第二部分(你刚刚添加的)与此无关。 - Luchian Grigore
2
一个左值表达式是一个指向对象的表达式,而 & 的结果是该对象的地址。但在这种情况下,没有对象。我不太确定标准实际上是怎么说的。 - Potatoswatter
@LuchianGrigore 默认的operator&在§5.3.1中由一元表达式语义描述。当operator&被重载时,它会转换为函数调用,并遵循函数调用的规则。 - Joseph Mansfield
1
在C++中,对象存储是不同的概念。对象必须存在于存储中,但是存储可以存在没有对象。对象可以被销毁而不释放存储(例如,这就是std::vector的实现方式,它获取存储并根据需要创建/移动/销毁存储中的对象)。 - M.M
显示剩余3条评论

4
假设x被初始化为一个有效的对象,然后被销毁,根据§3.8/6规定:
类似地,在对象的生命周期开始之前但在分配对象所需的存储空间之后,或者在对象的生命周期结束之后并在重新使用或释放对象所占用的存储空间之前,任何引用原始对象的glvalue都可以使用,但只能以有限的方式使用。对于正在构建或销毁的对象,请参见12.7。否则,这样的glvalue将引用已分配的存储空间(3.7.4.2),并且使用不依赖其值的glvalue属性是明确定义的。如果程序出现以下情况,则行为未定义:

— 对此类glvalue应用了lvalue-to-rvalue转换(4.1)

— 使用glvalue访问非静态数据成员或调用对象的非静态成员函数,或

— 将glvalue绑定到虚基类的引用(8.5.3)上,或

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

因此,简单地取地址是明确定义的,并且(参考相邻段落)甚至可以生产性地用于创建新对象以替代旧对象。
至于不取地址而只写x,那真的什么也不做,它是&x的一个合适子表达式。所以也没问题。

3
这意味着仅适用于“在重新使用或释放对象所占用的存储空间之前”。 - Angew is no longer proud of SO
1
@Angew 是的。我可以进一步追踪,但我的一般直觉是这种事情很冒险。 - Potatoswatter
这个答案涵盖了存储仍然存在的情况,但是如果存储不存在呢?(例如函数返回对局部变量的引用 - 当函数返回时,自动变量的存储被释放) - M.M
@M.M 我认为这需要一个内存模型规范,而据我所知,C++ 还没有这个规范。其他答案中 UB 的推理仍然存在一些漏洞。 - Potatoswatter

4
首先,非常有趣的问题。假设“悬空引用”表示“所引用的对象的生命周期已结束并且对象占用的存储器已被重用或释放”,我认为这是未定义的行为。我基于以下标准规定进行推理:
3.8 §3:
对象在其生命周期内仅适用于本国际标准中赋予对象的属性。[注:特别地,在对象的生命周期开始之前和结束之后,对对象的使用存在重大限制,如下所述...]
“如下所述”的所有情况均指:
在对象的生命周期开始之前,但在分配对象将占用的存储器之后,或者在对象的生命周期结束之后,并在重新使用或释放对象占用的存储器之前
1.3.24:
未定义的行为
这个国际标准没有规定任何要求的行为。[注:当这个国际标准省略任何明确的行为定义时,或者当程序使用错误的构造或错误的数据时,可能会出现未定义的行为...]。
我根据上述引文应用以下思路:
如果标准没有描述某种情况的行为,则行为是未定义的。
该标准仅描述了在其生命周期内的对象的行为以及在其生命周期开始/结束附近的一些特殊情况。所有这些情况都不适用于我们的悬空引用。
因此,以任何方式使用悬空引用都没有由标准规定的行为,因此该行为是未定义的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接