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

时间:2015-05-07 07:02:48

标签: c++ c++11 move

我有一个函数可以就地修改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
}

请注意,会返回右值引用

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

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

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

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

这使我相信,从std::string&左值参考版本的transform(...)返回值到std::string返回值,会发生副本。

这是对的吗?

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

4 个答案:

答案 0 :(得分:8)

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

  

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

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

  1. 参数不能是临时的(这正是rvalue引用所代表的)或
  2. 返回值不会保留在调用者上下文中的下一个分号之后(临时值被销毁时)。
  3. 如果调用者传递临时值并尝试保存结果,则会获得悬空引用。

      

    根据我的阅读,似乎共识是因为返回值是rvalues,再加上考虑RVO,只需按值返回就会有效:

    按值返回会添加移动构造操作。其成本通常与物体的大小成比例。虽然通过引用返回只需要机器确保一个地址在寄存器中,但按值返回需要将参数std::string中的几个指针归零并将它们的值放入要返回的新std::string中。

    它便宜,但非零。

    标准库目前采用的方向有点令人惊讶,是快速且不安全并返回参考。 (我知道实际执行此操作的唯一函数是来自std::get的{​​{1}}。)实际上,我已经向C ++核心语言委员会提交a proposal以解决此问题, a revision正在进行中,就在今天,我已开始调查实施情况。但它很复杂,而且不确定。

    <tuple>

    编译器不会在此处生成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 );
    

答案 1 :(得分:1)

以上版本的main() { TopologyBuilder builder = new TopologyBuilder(); builder.setSpout("num", new NumSpout(), 10); builder.setBolt("judge", new Bolt(), 3).shuffleGrouping("num"); builder.setBolt("odd", new Bolt_odd(), 2).shuffleGrouping("judge"); builder.setBolt("even", new Bolt_even(), 2).shuffleGrouping("judge"); } 的一些(非代表性)运行时:

run on coliru

transform

<强>输出

#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;
}

答案 2 :(得分:0)

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

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

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

把这个论点变成acount,我会说它将它作为引用传递而不是移动语义,因为它的后缀兼容,对于那些还没有掌握C ++ 11的开发人员更加理解。

答案 3 :(得分:0)

  

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

     

这是对的吗?

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

  

保持我的std :: string&amp;&amp;更好吗? transform(...)version?

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

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

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

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