我应该通过右值引用返回一个右值引用参数吗?

Posted

技术标签:

【中文标题】我应该通过右值引用返回一个右值引用参数吗?【英文标题】:Should I return an rvalue reference parameter by rvalue reference? 【发布时间】:2015-07-17 14:42:30 【问题描述】:

我有一个函数可以就地修改std::string&左值引用,返回对输入参数的引用:

std::string& transform(std::string& input)

    // transform the input string
    ...

    return input;

我有一个辅助函数,它允许对右值引用执行相同的内联转换:

std::string&& transform(std::string&& input)

    return std::move(transform(input)); // calls the lvalue reference version

注意它返回一个右值引用

我已经阅读了几个关于返回右值引用的 SO 问题(例如here 和here),并得出结论认为这是不好的做法。

根据我的阅读,似乎共识是,由于返回值右值,加上考虑到 RVO,仅按值返回将同样有效:

std::string transform(std::string&& input)

    return transform(input); // calls the lvalue reference version

但是,我还了解到返回函数参数会阻止 RVO 优化(例如 here 和 here)

这让我相信会从transform(...) 的左值引用版本的std::string& 返回值复制到std::string 返回值。

对吗?

保留我的std::string&& transform(...) 版本更好吗?

【问题讨论】:

附带说明,接受并返回普通&s 的原始函数非常讨厌——它改变了传递给它的对象,但它伪装成一个纯函数。这是误解的秘诀。这可能是难以找出“正确”方法来制作它的右值变体的原因。 返回用户已有的东西有什么意义?这不像你要链调用变换,是吗? @Drax,std::cout << foo(transform(get_str())); 呢? @SteveLorimer 足够公平:) 虽然不确定它是否证明了整个界面设计的合理性,但我也希望该函数在返回某些内容时复制字符串,对引用进行操作并返回它并不常见。但这似乎足够有效:) 【参考方案1】:

没有正确答案,但按值返回更安全。

我已经阅读了几个关于返回右值引用的 SO 问题,并得出结论认为这是不好的做法。

返回对参数的引用会向调用者强加一个合同

    参数不能是临时参数(这正是右值引用所代表的),或者 返回值不会保留在调用者上下文中的下一个分号之后(当临时对象被销毁时)。

如果调用者传递一个临时值并尝试保存结果,他们会得到一个悬空引用。

根据我的阅读,似乎共识是,由于返回值是右值,再加上考虑到 RVO,因此仅按值返回将同样有效:

按值返回添加了移动构造操作。这样做的成本通常与对象的大小成正比。而通过引用返回只需要机器确保一个地址在寄存器中,而按值返回需要将参数std::string中的一对指针归零并将它们的值放入一个新的std::string中以返回。

它很便宜,但不为零。

标准库目前采取的方向有点令人惊讶的是,快速且不安全并返回引用。 (我知道的唯一实际执行此操作的函数是 <tuple> 中的 std::get。)碰巧,我已将 a proposal 提交给 C++ 核心语言委员会以解决此问题,a revision 在有效,就在今天,我开始调查实施。但这很复杂,而且不确定。

std::string transform(std::string&& input)

    return transform(input); // calls the lvalue reference version

编译器不会在此处生成move。如果input 根本不是引用,而你做了return input; 它会,但没有理由相信transform 会因为它是一个参数而返回input,它不会推断无论如何,来自右值引用类型的所有权。 (参见 C++14 §12.8/31-32。)

你需要做的:

return std::move( transform( input ) );

或等效

transform( input );
return std::move( input );

【讨论】:

【参考方案2】:

上述transform 版本的一些(非代表性)运行时:

run on coliru

#include <iostream>
#include <time.h>
#include <sys/time.h>
#include <unistd.h>

using namespace std;

double GetTicks()

    struct timeval tv;
    if(!gettimeofday (&tv, NULL))
        return (tv.tv_sec*1000 + tv.tv_usec/1000);
    else
        return -1;


std::string& transform(std::string& input)

    // transform the input string
    // e.g toggle first character
    if(!input.empty())
    
        if(input[0]=='A')
            input[0] = 'B';
        else
            input[0] = 'A';
    
    return input;


std::string&& transformA(std::string&& input)

    return std::move(transform(input));


std::string transformB(std::string&& input)

    return transform(input); // calls the lvalue reference version


std::string transformC(std::string&& input)

    return std::move( transform( input ) ); // calls the lvalue reference version



string getSomeString()

    return string("ABC");


int main()

    const int MAX_LOOPS = 5000000;

    
        double start = GetTicks();
        for(int i=0; i<MAX_LOOPS; ++i)
            string s = transformA(getSomeString());
        double end = GetTicks();

        cout << "\nRuntime transformA: " << end - start << " ms" << endl;
    

    
        double start = GetTicks();
        for(int i=0; i<MAX_LOOPS; ++i)
            string s = transformB(getSomeString());
        double end = GetTicks();

        cout << "\nRuntime transformB: " << end - start << " ms" << endl;
    

    
        double start = GetTicks();
        for(int i=0; i<MAX_LOOPS; ++i)
            string s = transformC(getSomeString());
        double end = GetTicks();

        cout << "\nRuntime transformC: " << end - start << " ms" << endl;
    

    return 0;

输出

g++ -std=c++14 -O2 -Wall -pedantic -pthread main.cpp && ./a.out

Runtime transformA: 444 ms
Runtime transformB: 796 ms
Runtime transformC: 434 ms

【讨论】:

测量值是否会随着更大的字符串(不适合 SSO)而改变? @Hiura: 只需点击上面的 COLIRU 链接,按 Edit,然后根据需要更改代码并尝试一下。跨度> 【参考方案3】:

如果您的问题是纯优化导向的,最好不要担心如何传递或返回参数。编译器足够聪明,可以将您的代码延伸到纯引用传递、复制省略、函数内联甚至移动语义(如果它是最快的方法)。 基本上,在某些深奥的情况下,移动语义可以使您受益。假设我有一个将double** 作为成员变量的矩阵对象,并且该指针指向double 的二维数组。现在假设我有这个表达式:Matrix a = b+c; 复制构造函数(或赋值运算符,在这种情况下)将获得 bc 的总和作为临时变量,将其作为 const 引用传递,在 a 内部重新分配 m*namount of doubles指针,然后,它将在a+b sum-array 上运行,并将其值一一复制。 简单的计算表明它最多可以花费O(nm) 步骤(可以概括为O(n^2))。 move 语义只会将隐藏的double** 重新连接到a 内部指针中。它需要O(1)。 现在让我们考虑一下std::string: 将它作为引用传递需要O(1) 步骤(获取内存地址,传递它,取消引用它等,这在任何类型中都不是线性的)。 将其作为 r-value-reference 传递需要程序将其作为引用传递,重新连接隐藏的底层 C-char*,其中包含内部缓冲区,将原始缓冲区设为空(或在它们之间交换),复制 sizecapacity 以及更多操作。我们可以看到,虽然我们仍然在O(1) 区域 - 实际上可以有更多的步骤,而不是简单地将其作为常规参考传递。 好吧,事实是我没有对它进行基准测试,这里的讨论纯属理论。无论如何,我的第一段仍然是正确的。作为开发人员,我们假设了很多事情,但除非我们对所有事情进行基准测试,否则编译器在 99% 的时间里都比我们更清楚 考虑到这个论点,我会说将其保留为引用传递,而不是移动语义,因为它与 backword 兼容,并且对于尚未掌握 C++11 的开发人员来说更容易理解。

【讨论】:

从实用的角度来看,我同意你的看法。但是,我认为这仍然是一个非常好的问题,有助于理解 rValue 引用。我已经使用 rValue 引用有一段时间了,但是我仍然想更好地理解这方面:)。【参考方案4】:

这让我相信会从 std::string& 复制一份 将 transform(...) 的左值引用版本的返回值转换为 std::string 返回值。

对吗?

如果编译器不做 RVO,返回引用版本不会让 std::string 复制发生,但返回值版本会有复制。但是,RVO 有其局限性,因此 C++11 添加了 r 值引用和移动构造函数/赋值/std::move 来帮助处理这种情况。是的,RVO 比 move 语义更有效,move 比 copy 便宜但比 RVO 贵。

保留我的 std::string&& transform(...) 版本更好吗?

这有点有趣和奇怪。正如 Potatoswatter 回答的那样,

std::string transform(std::string&& input)

    return transform(input); // calls the lvalue reference version
 

您应该手动调用 std::move。

但是,您可以单击此 developerworks 链接:RVO V.S. std::move 以查看更多详细信息,这可以清楚地解释您的问题。

【讨论】:

以上是关于我应该通过右值引用返回一个右值引用参数吗?的主要内容,如果未能解决你的问题,请参考以下文章

为啥这个函数在给定右值参数的情况下返回一个左值引用?

右值引用,移动语义,完美转发

C11新特性右值引用&&

C++11 中的左值引用和右值引用的区别

这里应该使用右值引用吗?

在 C++ 中将右值引用转换为临时参数到 const 左值返回的正确方法