通过右值引用返回 vs 通过值返回

6

大多数 C++ 标准库实用程序在对 rvalue 限定符进行重载时,返回 rvalue 引用。例如,std::optional 对 value() 函数有以下重载。

constexpr T& value() &;
constexpr const T & value() const &;
constexpr T&& value() &&;
constexpr const T&& value() const &&;

这样做可以在需要时移动返回值,非常好。这是一种可靠的优化。

但是,与返回rvalue相关的不确定性如何处理呢?例如(live example here https://wandbox.org/permlink/kUqjfOWWRP6N57eS

auto get_vector() {
    auto vector = std::vector<int>{1, 2, 3};
    return std::optional{std::move(vector)};
}

int main() {
    for (auto ele : *get_vector()) {
        cout << ele << endl;
    }
}

上面的代码会导致未定义行为,因为for循环展开的方式不正确。
{
    auto && __range = range_expression ; 
    auto __begin = begin_expr ;
    auto __end = end_expr ;
    for ( ; __begin != __end; ++__begin) { 
        range_declaration = *__begin; 
        loop_statement 
    } 
} 

当绑定到*get_vector()的返回值时,转发引用range并没有扩展xvalue的生命周期。这会导致绑定到已销毁的值。因此会导致未定义行为。

为什么不通过返回值来内部移动存储的对象?尤其是现在C++17有prvalue优化的情况下。

auto lck = std::lock_guard{mtx};

请注意,这与此处的问题不同:C++11 rvalues and move semantics confusion (return statement),这个问题没有提到rvalue返回容器/持有者时寿命延长问题,并且在C++17强制使用prvalues省略之前就已经被问过了。

2
这是使用基于范围的for循环时遇到的一般问题,其中表达式是xvalue。值得一提的是,C++20有一个提案允许for (auto vec_op = get_vector(); auto ele : *vec_op),这将只需要略微更多的语法就可以实现正确版本的迭代。 - Kerrek SB
@kerreksb,我想象中可能有其他代码与当前的扩展类似。在这种情况下,返回值更好。为什么不将标准库组件更改为按值返回呢? - Curious
@KerrekSB,那个新扩展不会在表达式是左值时引起复制吗? - Curious
为了回答这两个问题:我认为可重用的库组件是由用户自行组合的,因此a)为什么要无条件地使一个通用组件变得低效,只因为你发现了一个你不喜欢的用法,b)你不会无条件地使用新语法:你会在需要时使用它(就像大多数代码编写一样)。 (我猜你可以将auto&&插入初始化器中以涵盖两种情况。) - Kerrek SB
@KerrekSB 将其更改为 auto&& 如何解决这里的问题?在这种情况下,寿命延长仍然不适用吗? - Curious
我的意思是你可以使用 for (auto&& x = f(); auto el : *x),而不必担心 f 返回的是左值还是右值。 - Kerrek SB
1个回答

6
为什么不返回值并在内部移动存储的对象?
因为这可能比返回引用更不高效。考虑一种情况,您使用返回的引用从该对象内部获取另一个引用。例如,像(*get_vector())[3]这样的情况。使用您提出的更改,它是原始副本的引用;当前的方式是对原始临时变量中的值的引用。
C ++作为一种语言,在处理对临时变量的引用的生命周期方面表现不佳。目前的解决方案要么是小心处理生命周期,要么不使用引用,并具有潜在的更慢/不高效的代码。标准库通常倾向于追求性能。

抱歉,有点困惑。为什么这会是对原始副本的引用?内部向量将被移动。 - Curious
向量的内容将被移动,但向量本身不会是同一个对象。此外,并非每个对象都比复制更便宜。 - Nicol Bolas
但是复制在哪里呢?所以如果我理解正确,你的意思是当你实际上不需要移动时,返回一个右值引用更有效,对吗? - Curious
@Curious:当你还不需要移动时。也就是说,作为表达式的一部分。对于成员子对象,“temporary.member_name”是一个xvalue。因此,如果您正在使用类似于“get”或“optional::value”或类似获取对象的概念成员的东西,则如果对象是临时的,则应该也获得xvalue。 - Nicol Bolas

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