我应该 return 右值引用的右值引用参数吗?

Should I return an rvalue reference parameter by rvalue reference?

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

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
}

注意它return是一个右值引用

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

从我读到的内容来看,似乎共识是因为 return 值 右值,加上考虑到 RVO,只是 return按价值计算将同样有效:

std::string transform(std::string&& input)
{
    return transform(input); // calls the lvalue reference version
}

但是,我还读到 returning 函数参数会阻止 RVO 优化(例如 here and here

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

对吗?

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

如果您的问题是纯优化导向的,最好不要担心如何传递或 return 一个参数。编译器足够聪明,可以将您的代码拉伸为纯引用传递、复制省略、函数内联,如果它是最快的方法,甚至可以移动语义。

基本上,移动语义在某些深奥的情况下可以使您受益。假设我有一个矩阵对象,其中包含 double** 作为成员变量,并且此指针指向 double 的二维数组。现在假设我有这个表达式:
Matrix a = b+c;
复制构造函数(或赋值运算符,在这种情况下)将获得 bc 的总和作为临时值,将其作为常量引用传递,重新分配 m*n 数量的 doublesa 内部指针上,然后,它将 运行 在 a+b 和数组上,并将其值一个一个地复制。 简单的计算表明它最多需要 O(nm) 步(可以归纳为 O(n^2))。移动语义只会将临时隐藏的 double** 重新连接到 a 内部指针。它需要 O(1)
现在让我们考虑一下 std::string: 将其作为引用传递需要 O(1) 个步骤(获取内存地址、传递它、取消引用等等,这在任何方面都不是线性的)。 将其作为 r-value-reference 传递需要程序将其作为引用传递,重新连接隐藏的底层 C-char* 保存内部缓冲区,将原始缓冲区设为空(或在它们之间交换),复制 sizecapacity 以及更多操作。我们可以看到,虽然我们仍处于 O(1) 区域 - 实际上可以有更多的步骤,而不是简单地将其作为常规参考传递。

好吧,事实是我没有对它进行基准测试,这里的讨论纯粹是理论上的。尽管如此,我的第一段仍然是正确的。作为开发人员,我们假设了很多事情,但除非我们对所有事情进行基准测试 - 编译器在 99% 的时间里比我们更清楚

考虑到这个论点,我会说将其保留为引用传递而不是移动语义,因为它与反向词兼容并且对于尚未掌握 C++11 的开发人员来说更容易理解。

没有正确答案,但 return按值计算更安全。

I have read several questions on SO relating to returning rvalue references, and have come to the conclusion that this is bad practice.

返回对参数的引用会将合同强加给调用者

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

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

From what I have read, it seems the consensus is that since return values are rvalues, plus taking into account the RVO, just returning by value would be as efficient:

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

它很便宜,但非零。

标准库目前采取的方向有点令人惊讶,即快速和不安全,return 参考。 (我知道实际执行此操作的唯一函数是 <tuple> 中的 std::get。)碰巧的是,我已经介绍了 a proposal to the C++ core language committee toward the resolution of this issue, a revision 正在开发中,就在今天我已经开始调查执行。但这很复杂,而且不确定。

std::string transform(std::string&& input)
{
    return transform(input); // calls the lvalue reference version
}

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

您需要做的事情:

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

或等同于

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

上述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

This leads me to believe a copy would happen from the std::string& return value of the lvalue reference version of transform(...) into the std::string return value.

Is that correct?

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

Is it better to keep my std::string&& transform(...) version?

这在某种程度上很有趣也很奇怪。正如 Potatoswatter 回答的那样,

std::string transform(std::string&& input)
{
    return transform(input); // calls the lvalue reference version
} 

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

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