首页 > 解决方案 > 为什么右值不能绑定到非 const 左值引用,除了写入临时值没有效果?

问题描述

我在这里阅读了SO问题并理解了答案的这一部分:“但是,如果您将临时对象绑定到非常量引用,则可以“永远”继续传递它,只是为了让您对对象的操作消失,因为某处一路上你完全忘记了这是暂时的。”

也就是说,在以下内容中:

#include <iostream>

void modifyValue(int& rValue) {
    rValue++;
}

int main() {
    modifyValue(9899);

    return 0;
}

如果右值可以绑定到非 const 左值引用,那么可能会进行许多最终会被丢弃的修改(因为右值是临时的),这是无用的。

然而,这似乎定义得很好(写入临时值就像写入任何值一样,生命周期与写入的有效性无关)。

这是禁止指定绑定的完全正确的理由(即使绑定会被很好地定义),但是一旦我认为禁止这种绑定会强制转发引用的需要,我的问题就开始形成了。

关于为什么右值不能绑定到非 const 左值引用,是否还有其他原因(即,除了写入临时值)?

标签: c++c++11rvalue-referencervaluelvalue

解决方案


简单的答案是,在大多数情况下,将临时值传递给期望可变左值引用的函数表示逻辑错误,而 c++ 语言正在尽最大努力帮助您避免出错。

函数声明:void foo(Bar& b)建议以下叙述:

foo 引用一个 Bar, b,它将对其进行修改。b因此既是输入又是输出

将临时值作为输出占位符传递通常比调用返回对象的函数更糟糕的逻辑错误,只是丢弃未经检查的对象。

例如:

Bar foo();

void test()
{
  /*auto x =*/ foo();  // probable logic error - discarding return value unexamined
}

但是,在这两个版本中,没有问题:

void foo(Bar&& b)

foo 获得 Bar 引用的对象的所有权

void foo(Bar b)

foo 在概念上获取 Bar 的副本,尽管在许多情况下编译器会决定创建和复制 Bar 是不必要的。

所以问题是,我们要达到什么目标?如果我们只需要一个 Bar 来工作,我们可以使用Bar&& borBar b版本。

如果我们想使用一个临时的并且可能使用一个现有的 Bar,那么我们很可能需要两个 的重载foo因为它们在语义上会略有不同:

void foo(Bar& b);    // I will modify the object referenced by b

void foo(Bar&& b);   // I will *steal* the object referenced by b

void foo(Bar b);   // I will copy your Bar and use mine, thanks

如果我们需要这种可选性,我们可以通过将一个包装在另一个中来创建它:

void foo(Bar& b)
{
  auto x = consult_some_value_in(b);
  auto y = from_some_other_source();
  modify_in_some_way(b, x * y);
}

void foo(Bar&& b)
{
  // at this point, the caller has lost interest in b, because he passed
  // an rvalue-reference. And you can't do that by accident.

  // rvalues always decay into lvalues when named
  // so here we're calling foo(Bar&)

  foo(b);   

  // b is about to be 'discarded' or destroyed, depending on what happened at the call site
  // so we should at lease use it first
  std::cout << "the result is: " << v.to_string() << std::endl;
}

有了这些定义,这些现在都是合法的:

void test()
{
  Bar b;
  foo(b);              // call foo(Bar&)

  foo(Bar());          // call foo(Bar&&)

  foo(std::move(b));   // call foo(Bar&&)
  // at which point we know that since we moved b, we should only assign to it
  // or leave it alone.
}

好吧,为什么这么关心?为什么在没有意义的情况下修改临时文件会是逻辑错误?

好吧,想象一下:

Bar& foo(Bar& b)
{
  modify(b);
  return b;
}

我们期望做这样的事情:

extern void baz(Bar& b);

Bar b;
baz(foo(b));

现在想象这可以编译:

auto& br = foo(Bar());

baz(br); // BOOM! br is now a dangling reference. The Bar no longer exists

因为我们被迫在特殊的重载中正确处理临时foo,所以作者foo可以确信这个错误永远不会在您的代码中发生。


推荐阅读