我应该 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;
复制构造函数(或赋值运算符,在这种情况下)将获得 b
和 c
的总和作为临时值,将其作为常量引用传递,重新分配 m*n
数量的 doubles
在 a
内部指针上,然后,它将 运行 在 a+b
和数组上,并将其值一个一个地复制。
简单的计算表明它最多需要 O(nm)
步(可以归纳为 O(n^2)
)。移动语义只会将临时隐藏的 double**
重新连接到 a
内部指针。它需要 O(1)
。
现在让我们考虑一下 std::string
:
将其作为引用传递需要 O(1)
个步骤(获取内存地址、传递它、取消引用等等,这在任何方面都不是线性的)。
将其作为 r-value-reference 传递需要程序将其作为引用传递,重新连接隐藏的底层 C-char*
保存内部缓冲区,将原始缓冲区设为空(或在它们之间交换),复制 size
和 capacity
以及更多操作。我们可以看到,虽然我们仍处于 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.
返回对参数的引用会将合同强加给调用者
- 参数不能是临时的(这正是右值引用所代表的),或者
- 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
版本的一些(非代表性)运行时:
#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 以查看更多详细信息,这清楚地解释了您的问题。
我有一个函数可以就地修改 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;
复制构造函数(或赋值运算符,在这种情况下)将获得 b
和 c
的总和作为临时值,将其作为常量引用传递,重新分配 m*n
数量的 doubles
在 a
内部指针上,然后,它将 运行 在 a+b
和数组上,并将其值一个一个地复制。
简单的计算表明它最多需要 O(nm)
步(可以归纳为 O(n^2)
)。移动语义只会将临时隐藏的 double**
重新连接到 a
内部指针。它需要 O(1)
。
现在让我们考虑一下 std::string
:
将其作为引用传递需要 O(1)
个步骤(获取内存地址、传递它、取消引用等等,这在任何方面都不是线性的)。
将其作为 r-value-reference 传递需要程序将其作为引用传递,重新连接隐藏的底层 C-char*
保存内部缓冲区,将原始缓冲区设为空(或在它们之间交换),复制 size
和 capacity
以及更多操作。我们可以看到,虽然我们仍处于 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.
返回对参数的引用会将合同强加给调用者
- 参数不能是临时的(这正是右值引用所代表的),或者
- 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
版本的一些(非代表性)运行时:
#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 以查看更多详细信息,这清楚地解释了您的问题。