题 我应该通过右值参考返回rvalue参考参数吗?


我有一个修改的功能 std::string& lvalue就地引用,返回对输入参数的引用:

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
}

注意它 返回右值引用

我已经阅读了几个关于返回右值参考的问题(这里 和 这里 例如),并得出结论,这是不好的做法。

从我所看到的,似乎共识是,因为返回值  rvalues,加上考虑到RVO,只是按值返回将是有效的:

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

但是,我还读到了返回函数参数会阻止RVO优化(例如 这里 和 这里

这让我相信一个副本会发生 std::string& 左值参考版本的返回值 transform(...) 进入 std::string 返回值。

那是对的吗?

保持我的生活是否更好? std::string&& transform(...) 版?


22
2018-05-07 07:02


起源


作为旁注,接受并返回普通的原始函数 &s非常讨厌 - 它会改变传递给它的对象,但它伪装成纯粹的函数。这是误解的一个秘诀。这可能是因为很难找出制造它的右值变体的“正确”方法。 - Daniel Earwicker
返回用户已有的东西有什么意义?这不像是你要进行连锁变换,是吗? - Drax
@Drax,怎么样 std::cout << foo(transform(get_str()));? - Steve Lorimer
@SteveLorimer足够公平:)不确定它是否证明整个界面设计是合理的,我也希望函数复制字符串,如果它返回一些东西,作用于引用并返回它并不常见。但它似乎足够有效:) - Drax


答案:


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

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

返回对参数的引用会使调用者的合同成为其中之一

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

如果调用者传递临时值并尝试保存结果,则会获得悬空引用。

从我所看到的,似乎共识是因为返回值是rvalues,再加上考虑RVO,只需按值返回就会有效:

按价值返回会增加移动建设操作。其成本通常与物体的大小成比例。而通过引用返回只需要机器确保一个地址在寄存器中,按值返回需要将参数中的几个指针归零 std::string 并把他们的价值观放在一个新的 std::string 被退回。

它很便宜,但非零。

有些令人惊讶的是,标准库当前采用的方向是快速且不安全并返回参考。 (我知道的唯一功能就是这样做 std::get 从 <tuple>。)碰巧,我已经提出过了 一份提案 向C ++核心语言委员会提出解决此问题的方法, 修订 正在进行中,直到今天我才开始研究实施。但它很复杂,而且不确定。

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

编译器不会生成 move 这里。如果 input 根本不是参考,你做到了 return input; 它会,但它没有理由相信 transform将返回 input 只是因为它是一个参数,并且它不会从rvalue引用类型中推断出所有权。 (见C ++14§12.8/ 31-32。)

你需要这样做:

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

或者等价的

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

8
2018-05-07 08:44





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

跑在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

1
2018-05-07 09:46



度量是否随着更大的字符串(不适合SSO)而变化? - Hiura
@Hiura:请关注 COLIRU 链接上面,按 编辑,并根据需要更改代码并尝试一下。 - j.holetzeck


如果您的问题是纯粹的优化导向,最好不要担心如何传递或返回参数。编译器足够智能,可以将代码扩展为纯引用传递,复制省略,函数内联,甚至移动语义,如果它是最快的方法。

基本上,移动语义可以在一些深奥的案例中受益。假设我有一个矩阵对象 double** 作为一个成员变量,这个指针指向一个二维数组 double。现在让我说我有这个表达式:
Matrix a = b+c;
复制构造函数(或者是分配运算符,在这种情况下)将获得总和 b 和 c 作为temorary,将其作为const引用传递,重新分配 m*n数量 doubles 上 a 内部指针,然后,它将运行 a+b sum-array并将逐个复制其值。 简单的计算表明它可以占用 O(nm) 步骤(可以归结为 O(n^2))。移动语义只会重新连接隐藏的内容 double** 走出了时代 a 内部指针。它需要 O(1)
现在让我们考虑一下 std::string 一会儿: 传递它作为参考 O(1) 步骤(获取内存地址,传递它,取消引用它等,这不是任何形式的线性)。 将其作为r值引用传递需要程序将其作为参考传递,重新连接隐藏的底层C-char* 它保存内部缓冲区,使原始缓冲区为空(或在它们之间交换),复制 size 和 capacity 还有更多的行动。我们可以看到,虽然我们还在 O(1) zone - 除了简单地将其作为常规引用传递之外,还可以实现更多步骤。

好吧,事实是我没有对它进行基准测试,这里的讨论纯粹是理论上的。从来没有,我的第一段仍然是真的。我们假设许多东西都是开发人员,但除非我们将所有东西都标记为死亡 - 编译器在99%的时间内比我们更清楚

把这个参数考虑进去,我会说保留它作为引用传递而不是移动语义,因为它的后缀兼容,而且对于那些还没有掌握C ++ 11的开发人员更加理解。


0
2018-05-07 08:03



我从实际角度同意你的观点。但是,我认为这仍然是一个非常好的问题,有助于理解rValue引用。我一直在与rValue refences工作很长一段时间但是这方面我还是想更好地理解:)。 - laurisvr


这让我相信一个副本会发生在std :: string&   将transform(...)的左值引用版本的值返回到   std :: string返回值。

那是对的吗?

返回引用版本不会让std :: string复制,但如果编译器没有执行RVO,则返回值版本将具有副本。但是,RVO有其局限性,因此C ++ 11添加了r值引用并移动构造函数/赋值/ std :: move来帮助处理这种情况。是的,RVO比移动语义更有效,移动比复制更便宜但比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 ::移动 查看更多细节,清楚地解释您的问题。


0
2017-11-12 14:44