我应该通过右值引用返回一个右值引用参数吗?
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;
复制构造函数(或赋值运算符,在这种情况下)将获得 b
和 c
的总和作为临时变量,将其作为 const 引用传递,在 a
内部重新分配 m*n
amount 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*
,其中包含内部缓冲区,将原始缓冲区设为空(或在它们之间交换),复制 size
和capacity
以及更多操作。我们可以看到,虽然我们仍然在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 以查看更多详细信息,这可以清楚地解释您的问题。
【讨论】:
以上是关于我应该通过右值引用返回一个右值引用参数吗?的主要内容,如果未能解决你的问题,请参考以下文章