【问题标题】:Dangling reference when returning reference to reference parameter bound to temporary返回对绑定到临时的引用参数的引用时悬空引用
【发布时间】:2021-10-13 02:10:54
【问题描述】:

这个问题是指 Howard Hinnant 对问题Guaranteed elision and chained function calls 的回答。

在他的回答的底部,他说:

请注意,在此最新设计中,如果您的客户曾经这样做:

X&& x = a + b + c;

那么 x 是一个悬空引用(这就是 std::string 不这样做的原因)。

cppreference.com 上的文章“引用初始化”的“临时对象的生命周期”段落列出了绑定到引用的临时对象生命周期规则的例外情况。一个是:

“函数调用中引用参数的临时绑定一直存在到包含该函数调用的完整表达式的末尾:如果函数返回的引用比完整表达式的寿命长,则它成为悬空引用。”

我认为它的意思是“如果函数返回一个引用到引用参数绑定到的临时对象”,而不仅仅是其他一些引用。因此,我认为这就是解释 Howard Hinnant 上述陈述的规则。

以下示例基于我所指问题中给出的示例:

struct X
{
    int _x;
    X() : _x(0) {}
    X(int x) : _x(x) {}
    X(X const& other) : _x(other._x) {}
    X(X&& other) noexcept : _x(other._x) { other._x = 0; std::cout << "Move from " << &other << " to " << this << std::endl; }
    X& operator+=(const X& other) { _x += other._x; return *this; }

    friend X operator+(X const& lhs, X const& rhs)
    {
        std::cout << "X const& lhs: " << &lhs << std::endl;
        X temp = lhs;
        temp += rhs;
        return temp;
    }

    friend X&& operator+(X&& lhs, X const& rhs)
    {
        std::cout << "X&& lhs: " << &lhs << std::endl;
        lhs += rhs;
        return std::move(lhs);
    }
};

int anotherFunc(int a)
{
    int bigArray[3000]{};
    std::cout << "ignore:" << &bigArray << std::endl;
    int b = a * a;
    std::cout << "int b: " << &b << std::endl;
    return 2 * b;
}

int main()
{
    X a(1), b(2), c(3), d(4);
    X&& sum = a + b + c + d;
    std::cout << "X&& sum: " << &sum << std::endl;
    anotherFunc(15);
    std::cout << "sum._x: " << sum._x << std::endl;

    return 0;
}

打印出来

X const& lhs: 000000907DAFF8B4
Move from 000000907DAFF794 to 000000907DAFFA14
X&& lhs: 000000907DAFFA14
X&& lhs: 000000907DAFFA14
X&& sum: 000000907DAFFA14
ignore:000000907DAFC360
int b: 000000907DAFF254
sum._x: 10

使用 MSVC 编译时;以及使用 gcc 或 clang 编译时的类似输出。

sum 在这里应该是一个悬空引用。尽管如此,仍在打印正确的值“10”。它甚至可以在sum 的引用初始化和通过所述引用进行访问之间将一个大数组推入堆栈时工作。用于 sum 引用的临时对象的内存不会被重用,并且总是分配到其他地方(与以下函数调用的堆栈帧相关),无论下一个堆栈帧有多大或多小。

为什么我测试过的每个编译器都会保留X&amp;&amp; operator+(X&amp;&amp; lhs, X const&amp; rhs) 本地的临时对象,即使根据cppreference.com 的规则,sum 应该是一个悬空引用。或者,更准确地说:尽管访问悬空引用是未定义的行为,但为什么每个编译器都以这种方式实现它?

【问题讨论】:

  • 您预计会发生什么?您无法观察到 absence 未定义的行为,打印“10”是其无数可能的表现之一。
  • 我原以为这些编译器中的任何一个都会重用用于 sum 绑定到的临时对象的堆栈内存。如果没有规则要求他们保留该临时对象,为什么他们会全部保留?
  • 未定义行为意味着程序不受(逻辑)分析,任何尝试都是浪费时间和精力。您所能做的就是移除 UB 并重新测试。另见Old New Thing - Undefined behavior can result in time travel (among other things, but time travel is the funkiest)
  • 因为函数栈地址空间如果通常只在入口处计算一次并且通常不会与内部函数调用重叠(但可以有尾递归优化等)。如果您还打印&amp;bigArray[3000],您会注意到它与sum 地址不重叠。您可能会看到悬空的参考值被覆盖(如果足够幸运并且没有启用优化),如下所示:X *psum; { X&amp;&amp; sum = a + b + c + d; psum = &amp;sum; } { int v = 42; std::cout &lt;&lt; "&amp;v = " &lt;&lt; &amp;v &lt;&lt; " v = " &lt;&lt; v &lt;&lt; std::endl; } std::cout &lt;&lt; "sum._x: " &lt;&lt; psum-&gt;_x &lt;&lt; std::endl;
  • @Ruperrrt 是否重用临时保留的堆栈空间完全取决于实现。此外,ABI 通常会规定一些堆栈对齐方式,这可能会导致 anotherFunc 的堆栈帧低于您的预期。这是具有定义行为的现场演示:godbolt.org/z/v7q1vsK1h。请注意,rsp+15 的临时堆栈存储在其生命周期结束后不会在 g1 调用中重用。

标签: c++ pass-by-reference temporary-objects return-by-reference


【解决方案1】:

我喜欢在这种情况下保留一个示例class AA 的完整定义有点冗长,无法在此处列出,但完整包含在 at this link 中。

简而言之,A 保留一个state 和一个status,而status 可以是这些枚举之一:

    destructed             = -4,
    self_move_assigned     = -3,
    move_assigned_from     = -2,
    move_constructed_from  = -1,
    constructed_specified  =  0

即特殊成员相应地设置状态。例如~A() 看起来像这样:

~A()
{
    assert(is_valid());
    --count;
    state_ = randomize();
    status_ = destructed;
}

还有一个流式操作符可以打印出这个类。

语言律师免责声明: 打印出被破坏的A 是未定义的行为,任何事情都可能发生。话虽如此,当在关闭优化的情况下编译实验时,您通常会得到预期的结果。

对我来说,在-O0 使用clang,这样:

#include "A.h"
#include <iostream>

int
main()
{
    A a{1};
    A b{2};
    A c{3};
    A&& x = a + b + c;
    std::cout << x << '\n';
}

输出:

destructed: -1002199219

将行改为:

    A x = a + b + c;

结果:

6

【讨论】:

  • 感谢您回复我的问题!您的课程非常适合检查此类行为。
猜你喜欢
  • 1970-01-01
  • 2018-11-05
  • 2017-07-09
  • 1970-01-01
  • 2012-07-18
  • 2021-05-30
  • 1970-01-01
  • 1970-01-01
  • 2010-11-23
相关资源
最近更新 更多