rvalue引用允许悬空引用吗?

35

请考虑以下内容。

#include <string>
using std::string;

string middle_name () {
    return "Jaan";
}

int main ()
{
    string&& danger = middle_name();   // ?!
    return 0;
}

这段代码没有计算任何东西,但是编译没有错误,它展示了一些让我困惑的东西:danger 是一个悬空引用,不是吗?


2
当你可以在左侧使用&&时,这让我感到困惑,那会有什么用处呢? - Viktor Sehr
2
@Viktor:一个右值引用可以绑定到一个临时对象,仍然是可修改的。例如 int &&variable_or_dummy = modify_var? move(var) : int(); - Potatoswatter
1
一旦您给一个右值命名,它就变成了左值,因此在主函数中,“danger”是一个左值。 rvalueness或lvalueness是表达式的属性。 - Peter McG
4个回答

46

右值引用是否允许悬垂引用?

如果你的意思是"是否可能创建悬垂的右值引用",那么答案是肯定的。然而,你的例子并没有造成这种情况。

string middle_name () {
    return "Jaan";
}

int main()
{
    string&& nodanger = middle_name();   // OK.
    // The life-time of the temporary is extended
    // to the life-time of the reference.
    return 0;
}

是完全没有问题的。同样的规则也适用于这里,使得这个例子(Herb Sutter的文章)也是安全的。如果你使用纯右值来初始化引用,则临时对象的生命周期将延长到引用的生命周期。但是,你仍然可以产生悬空引用。例如,下面的代码就不再安全:

int main()
{
    string&& danger = std::move(middle_name());  // dangling reference !
    return 0;
}

因为 std::move 返回的是一个string&&(它不是纯正rvalue),所以延长临时对象寿命的规则不适用。在这里,std::move 返回了一个被称为xvalue的东西。一个xvalue只是一个未命名的rvalue引用。因此,它可以指代任何东西,基本上无法猜测返回的引用指向什么,除非查看函数的实现。


5
优秀的意图却走错了路的典型案例!“move”在此处指行动或举措。 - Potatoswatter
3
为什么这需要“纯”右值的直觉?xvalue情况似乎完全合理... - Andres Jaan Tack
6
@Andres:我在答案中尝试解释了这个问题。只有当初始化程序是一个纯右值时,编译器才确切知道要初始化的引用将引用什么。在这种情况下,正确的临时对象寿命可以轻松地被扩展。如果初始化程序本身是一个引用,在一般情况下你不会真正知道它指的是哪个对象。对于编译器来说,std::move 没有实际意义。std::move 的结果引用同一对象的信息在函数类型中不会被保留。 - sellibitze
3
@sellibitze 的评论很有道理!一个被引用的对象可以拥有任何一种生命周期,而纯右值的生命周期则很容易预测,因此可以轻松地进行扩展。 - Andres Jaan Tack
2
请参见https://dev59.com/w5_ha4cB1Zd3GeqPzHdk#42441832,值类别并不决定对象的生命周期是否会被延长。右侧的值是xvalue还是其他类型并不能充分说明rvalue的生命周期是否会被延长。您能否更新答案,包括实际扩展对象生命周期的地方,以免误导他人? - Curious
显示剩余2条评论

17

rvalue引用绑定到rvalue。 rvalue可以是prvaluexvalue [解释]。绑定到前者永远不会创建悬空引用,而绑定到后者可能会。这就是为什么通常不建议选择T&&作为函数的返回类型。 std::move是此规则的例外。

T&  lvalue();
T   prvalue();
T&& xvalue();

T&& does_not_compile = lvalue();
T&& well_behaved = prvalue();
T&& problematic = xvalue();

+1 你说得对,不编写返回右值引用的函数是一个好的经验法则。std::move和std::forward是明显的例外。 - sellibitze

3

danger是一个悬空引用,对吗?

与使用const &一样,并不会出现悬空引用的情况:danger将拥有这个右值。


0
当然,右值引用仍然是引用,因此它也可能会悬空。你只需要让编译器处于必须拖动引用的情况,并且同时逃逸所引用值的作用域,就像这样:

演示

#include <cstdio>
#include <tuple>

std::tuple<int&&> mytuple{ 2 };

auto pollute_stack()
{
    printf("Dumdudelei!\n");
}

int main()
{
    {
        int a = 5;
        mytuple = std::forward_as_tuple<int&&>(std::move(a));
    }
    pollute_stack();
    int b = std::get<int&&>(mytuple);
    printf("Hello b = %d!\n", b);
}

输出:

Dumdudelei!
Hello b = 0!

正如你所看到的,b现在有了错误的值。为什么?我们将一个右值引用赋给了自动变量a,并将其放入全局元组中。然后我们逃离了a的作用域,并通过std::get<int&&>检索其值,这将评估为右值引用。因此,新对象b实际上是从a移动构造的,但编译器找不到a,因为它的作用域已经结束了。因此,std::get<int&&>评估为0(尽管它可能是未定义行为,并且可以评估为任何内容)。

请注意,如果我们不触及堆栈,右值引用实际上仍然会在其作用域结束后找到对象a的原始值,并检索正确的值(只需尝试并取消注释pollute_stack(),看看会发生什么)。pollute_stack()函数只是通过printf()执行一些与io相关的操作,同时将堆栈指针向前和向后移动,写入堆栈值。

然而,编译器完全无法看穿这一点,因此请注意这一点。


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