【问题标题】:Dangling references and undefined behavior悬空引用和未定义行为
【发布时间】:2013-01-21 17:42:11
【问题描述】:

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

&x;

甚至

x;

?

【问题讨论】:

  • 我最好的猜测是它不是 UD。 *x; 绝对是。
  • x 是一个参考,所以*x... 不合法。
  • Jan Dvorak:你怎么认为 &x 很好?特别是如果引用对象的类型是一个重载 operator& 的类,它肯定是未定义的行为;标准中的任何内容都没有让我认为它已经定义,即使事实并非如此。
  • @davmac 好点。

标签: c++ reference undefined-behavior language-lawyer


【解决方案1】:

首先,非常有趣的问题。

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

3.8 §3:

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

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

在对象的生命周期开始之前,但在对象将占用的存储空间已经结束之后 分配 38 或者,在对象的生命周期结束之后并且在对象占用的存储空间之前 重复使用或释放​​

1.3.24: 未定义的行为

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

我将以下思路应用于上述引用:

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

【讨论】:

    【解决方案2】:

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

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

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

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

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

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

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

    至于获取地址并只写x,那真的什么都做不了,它是&x的正确子表达式。所以也没关系。

    【讨论】:

    • 这意味着这只适用于“在对象占用的存储空间被重用或释放之前”。
    • @Angew 是的。假设我可以进一步追查,但我的普遍直觉是这种事情是在诱惑命运。
    • 这个答案涵盖了存储仍然存在的情况,但是不存在的情况呢? (例如,函数返回对局部变量的引用 - 函数返回时为自动变量释放存储空间)
    • @M.M 这需要一个内存模型规范,据我所知,C++ 仍然缺乏该规范。 UB 的推理在此处其他答案的“裂缝之间”仍然成立。
    【解决方案3】:

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

    如果泛左值所引用的对象不是 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 推断,如果没有指定,那么它期望一个纯右值:

    当一个泛左值表达式作为一个操作数的操作数出现时,该操作数需要一个纯右值,左值到右值 (4.1)、数组到指针 (4.2) 或函数到指针 (4.3)应用标准转换将表达式转换为纯右值。

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

    【讨论】:

    • 不过,& 运算符可以重载。
    • 答案的第二部分(您刚刚添加的)不相关。
    • @davmac 我希望这是对这个问题的安全假设!
    • @LuchianGrigore 我认为这对问题的其他读者很有用。
    • 左值是引用对象的表达式,&的结果是对象的地址。但在这种情况下,没有对象。我不太确定标准的真正含义。
    猜你喜欢
    • 2023-03-21
    • 1970-01-01
    • 2018-08-30
    • 1970-01-01
    • 2011-10-11
    • 1970-01-01
    • 1970-01-01
    • 2018-08-19
    相关资源
    最近更新 更多