一个 RValue 引用返回有什么实际用处吗?

82
当函数需要返回RValue引用时,是否有特定的原因?这是一种技巧、诀窍、成语或模式吗?
MyClass&& func( ... );

我知道一般情况下返回引用的危险性,但有时我们仍然这样做,不是吗?T& T::operator=(T)只是一个惯用的例子。但是T&& func(...)呢?是否有任何普遍的地方可以从中获益?与编写库或API代码相比,与仅客户端代码相比可能不同。
5个回答

62

有一些情况下这种写法是合适的,但这种情况相对来说比较少见。一个例子就是当你想要允许客户端从数据成员中移动时会出现这种情况,例如:

template <class Iter>
class move_iterator
{
private:
    Iter i_;
public:
    ...
    value_type&& operator*() const {return std::move(*i_);}
    ...
};

5
一个很好的例子。这种模式就是,你希望客户端代码去“移动”某些东西--允许他去“窃取”。当然,是的。 - towi
4
通常情况下,当一个对象在 std::lib 中使用时,其移动后的状态必须符合该 std::lib 的所有规定要求。std-defined 类型还必须保证其移动后的状态是有效的。只要在调用函数时没有对该对象的值有前置条件限制,客户端就可以使用该对象调用任何函数。 - Howard Hinnant
4
在上述示例中,没有被移动的对象。std::move不会移动���何东西,它只是将对象转换为右值。移动(或不移动)该右值取决于客户端。如果客户端在迭代器遍历之间两次解引用移动迭代器,那么该客户端将仅访问已移动的值。 - Howard Hinnant
3
使用value_type而不是value_type&&作为返回类型会更安全吗? - fredoverflow
1
是的,我也这么认为。但在这种情况下,我认为增加的好处大于风险。move_iterator经常用于通用代码中,将复制算法转换为移动算法(例如vector :: insert的移动版本)。如果您向通用代码提供具有昂贵复制和移动的类型,则会额外添加一个无谓的副本。例如,我考虑array<int,N>。当将一堆这些插入到vector中时,您不希望意外引入额外的副本。在风险方面,“const X&x = * i”非常罕见。我从未见过。 - Howard Hinnant
显示剩余9条评论

17

这是对towi的评论的补充。您永远不希望返回对本地变量的引用。但是您可能会遇到这种情况:

vector<N> operator+(const vector<N>& x1, const vector<N>& x2) { vector<N> x3 = x1; x3 += x2; return x3; }
vector<N>&& operator+(const vector<N>& x1, vector<N>&& x2)    { x2 += x1; return std::move(x2); }
vector<N>&& operator+(vector<N>&& x1, const vector<N>& x2)    { x1 += x2; return std::move(x1); }
vector<N>&& operator+(vector<N>&& x1, vector<N>&& x2)         { x1 += x2; return std::move(x1); }

在除了两个参数都是左值的情况下,这应该可以防止任何副本(和可能的分配)。


1
虽然这是可能的,但一般不建议这样做,因为这种方法除了节省临时变量之外还有自己的问题。请参见https://dev59.com/D2025IYBdhLWcg3wblg3。 - sellibitze
2
这些返回调用需要使用std::move()吗? - wjl
1
@wjl:好问题,但我不这么认为。std::move可以在不使用std::move的情况下工作。我认为在这里,将其转换为&&就可以了。 - Clinton
1
@Clinton 在您的代码中没有强制类型转换,您必须使用return std::move(x2);等方式。或者您可以编写一个到右值引用类型的强制类型转换,但这就是move所做的事情。 - M.M
1
只有在返回值未被使用或被分配给一个对象时,代码才是正确的——但这时你可能会选择按值返回并按值获取参数,并让复制省略发挥作用。 - M.M
显示剩余3条评论

8

不需要返回引用,只需返回值即可。一般情况下,返回引用并不危险——而返回对于局部变量的引用则是危险的。然而,返回一个右值引用在几乎所有情况下都是毫无意义的(我猜如果你正在编写std::move之类的东西可能会有用)。


5
我认为在设计C++0x的早期阶段,曾经有人建议move-assignT&& operator+(const T&,T&&)等内容应返回&&。但现在已经没有了,在最终草案中被删除了。这就是我问的原因。 - towi

2

如果您确定在函数退出后,引用的对象不会超出范围(例如全局对象的引用或返回类字段引用的成员函数等),则可以通过引用返回。

引用返回规则对左值和右值引用是相同的,区别在于您希望如何使用返回的引用。就我所见,通过右值引用返回很少见。

如果您有一个函数:

Type&& func();

您不会喜欢这样的代码:
Type&& ref_a = func();

因为它有效地将ref_a定义为Type&,因为命名的rvalue引用是一个lvalue,并且在这里不会执行任何实际的移动操作。 这很像:

const Type& ref_a = func();

除了实际的ref_a是一个非const的左值引用之外,这与前面的例子类似。

即使你直接将func()传递给另一个需要Type&&参数的函数,它也不是很有用,因为在该函数内部它仍然是一个命名引用。

void anotherFunc(Type&& t) {
  // t is a named reference
}
anotherFunc(func());

func()和anotherFunc()之间的关系更像是一种“授权”,即func()同意anotherFunc()可能取得(或者说“窃取”)从func()返回的对象的所有权。但这种协议非常宽松。非const左值引用仍然可以被调用者“窃取”。实际上,很少有函数定义为接受rvalue引用参数。最常见的情况是“anotherFunc”是一个类名,而anotherFunc()实际上是一个移动构造函数。


0

还有一种可能的情况:当您需要解包元组并将值传递给函数时。

在这种情况下,如果您不确定复制省略是否有效,则可能会很有用。

例如:

template<typename ... Args>
class store_args{
    public:
        std::tuple<Args...> args;

        template<typename Functor, size_t ... Indices>
        decltype(auto) apply_helper(Functor &&f, std::integer_sequence<size_t, Indices...>&&){
            return std::move(f(std::forward<Args>(std::get<Indices>(args))...));
        }

        template<typename Functor>
        auto apply(Functor &&f){
            return apply_helper(std::move(f), std::make_index_sequence<sizeof...(Args)>{});
        }
};

这种情况相当罕见,除非你正在编写某种形式的std::bindstd::thread替代品。


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