假设一个悬空的参考x
。只写是未定义的行为吗
&x;
甚至
x;
?
首先,非常有趣的问题。
我会说这是未定义的行为,假设“悬空引用”意味着“被引用对象的生命周期已经结束,并且对象占用的存储空间已被重用或释放。” 我的推理基于以下标准裁决:
3.8 §3:
本国际标准中赋予对象的属性仅在其生命周期内适用于给定对象。[注意:特别是,在对象的生命周期开始之前和其生命周期结束之后,对对象的使用有很大的限制,如下所述...]
所有“如下所述”的情况均指
在对象的生命周期开始之前但在对象将占用的存储空间分配之后38,或者在对象的生命周期结束之后并且在对象占用的存储空间被重用或释放之前
1.3.24: 未定义的行为
本国际标准对其没有要求的行为 [注:当本国际标准省略任何明确的行为定义或程序使用错误构造或错误数据时,可能会出现未定义的行为。...]
我将以下思路应用于上述引文:
假设x
使用有效对象进行初始化,然后将其销毁,则适用 §3.8/6:
类似地,在对象的生命周期开始之前但在对象将占用的存储空间分配之后,或者在对象的生命周期结束之后并且在对象占用的存储空间被重用或释放之前,引用的任何泛左值可以使用原始对象,但只能以有限的方式使用。对于正在建造或破坏的物体,见 12.7。否则,这样的glvalue指的是分配的存储(3.7.4.2),并且使用不依赖于其值的glvalue的属性是明确定义的。如果出现以下情况,该程序具有未定义的行为:
— 左值到右值的转换(4.1)应用于这样的左值,
— glvalue 用于访问非静态数据成员或调用对象的非静态成员函数,或
— glvalue 绑定到对虚拟基类 (8.5.3) 的引用,或
— glvalue 用作 dynamic_cast (5.2.7) 的操作数或 typeid 的操作数。
因此,简单地获取地址是明确定义的,并且(参考相邻的段落)甚至可以有效地用于创建一个新对象来代替旧对象。
至于不取地址,只写x
,那真的什么都做不了,它是 的适当子表达式&x
。所以也没关系。
使用无效对象(引用、指针等)未定义行为的原因是左值到右值的转换(第 4.1 节):
如果泛左值引用的对象不是 T 类型的对象,也不是从 T 派生的类型的对象,或者如果该对象未初始化,则需要此转换的程序具有未定义的行为。
假设我们没有重载operator&
,一元运算&
符将左值作为其操作数,因此不会发生转换。只有一个标识符,如 inx;
也不需要转换。当引用被用作表达式中的操作数时,您只会得到未定义的行为,该表达式期望该操作数是右值 - 大多数运算符都是这种情况。关键是,做&x
实际上并不需要访问x
. 左值到右值的转换发生在那些需要访问其值的运算符中。
我相信您的代码定义明确。
当operator&
被重载时,表达式&x
被转换为函数调用并且不遵守内置运算符的规则 - 相反它遵循函数调用的规则。对于&x
,转换为函数调用的结果是x.operator&()
或operator&(x)
。在第一种情况下,x
当使用类成员访问运算符时,将发生左值到右值的转换。在第二种情况下,参数operator&
将被复制初始化x
(如T arg = x
),其行为取决于参数的类型。例如,在参数是左值引用的情况下,没有未定义的行为,因为不会发生左值到右值的转换。
因此,如果operator&
为 的类型重载x
,则代码可能定义明确,也可能不明确,具体取决于operator&
函数的调用。
您可能会争辩说,一元运算&
符依赖于至少有一些您拥有地址的有效存储区域:
否则,如果表达式的类型为
T
,则结果的类型为“pointer toT
”,并且是一个纯右值,即指定对象的地址
一个对象被定义为一个存储区域。在所引用的对象被销毁后,该存储区域不再存在。
我更愿意相信,如果实际访问了无效对象,它只会导致未定义的行为。引用仍然认为它指的是某个对象,即使它不存在,它也可以愉快地给出它的地址。但是,这似乎是标准中指定不明确的部分。
作为未定义行为的示例,请考虑x + x
. 现在我们遇到了标准中另一个指定不明确的部分。的操作数的值类别+
未指定。通常从 §5/8 中推断,如果未指定,则它需要一个纯右值:
每当一个泛左值表达式作为操作数的操作数出现时,该操作数需要一个纯右值,左值到右值 (4.1)、数组到指针 (4.2) 或函数到指针 (4.3) 标准转换是应用于将表达式转换为纯右值。
现在因为x
是左值,所以需要左值到右值的转换,我们得到未定义的行为。这是有道理的,因为加法需要访问 的值,x
因此它可以计算出结果。