[英]Should I return an rvalue reference parameter by rvalue reference?
我有一個函數,它可以就地修改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
}
請注意,它返回一個右值引用 。
我已經閱讀了關於返回右值引用的SO的幾個問題( 這里和這里例如),並得出結論這是不好的做法。
根據我的閱讀,似乎共識是因為返回值是 rvalues,再加上考慮RVO,只需按值返回就會有效:
std::string transform(std::string&& input)
{
return transform(input); // calls the lvalue reference version
}
但是,我還讀到返回函數參數會阻止RVO優化(例如此處和此處 )
這讓我相信一個副本會從std::string&
lvalue引用版本的transform(...)
的返回值發生到std::string
返回值。
那是對的嗎?
保持我的std::string&& transform(...)
版本會更好嗎?
沒有正確的答案,但按價值返回更安全。
我已經閱讀了幾個關於返回左值引用的SO的問題,並得出結論這是不好的做法。
返回對參數的引用會使調用者的合同強制轉向
如果調用者傳遞臨時值並嘗試保存結果,則會獲得懸空引用。
根據我的閱讀,似乎共識是因為返回值是rvalues,再加上考慮RVO,只需按值返回就會有效:
按值返回會增加移動構建操作。 其成本通常與物體的大小成比例。 而通過引用返回只需要機器確保一個地址在寄存器中,按值返回需要將參數std::string
的幾個指針歸零並將它們的值放入要返回的新std::string
中。
它很便宜,但非零。
有些令人驚訝的是,標准庫目前采用的方向是快速且不安全並返回參考。 (我知道實際執行此操作的唯一函數是std::get
from <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 );
上述版本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
如果你的問題是純粹的優化導向,最好不要擔心如何傳遞或返回一個參數。 編譯器非常智能,可以將代碼擴展為純引用傳遞,復制省略,函數內聯,甚至移動語義,如果它是最快的方法。
基本上,移動語義可以在一些深奧的案例中受益。 讓我們說我有保持矩陣對象double**
作為成員變量和該指針指向的兩個dimenssional陣列double
。 現在讓我說我有這個表達式:
Matrix a = b+c;
一個拷貝構造(或分配新建分配FY操作者,在這種情況下)將得到的總和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)
區域 - 但實際上可以有更多步驟而不是簡單地將其作為常規參考傳遞。
好吧,事實是我沒有對它進行基准測試,這里的討論純粹是理論上的。 從來沒有,我的第一段仍然是真的。 我們假設許多東西都是開發人員,但除非我們將所有東西都標記為死亡 - 編譯器在99%的時間內比我們更清楚
把這個參數考慮進去,我會說把它作為參考通道而不是移動語義,因為它的后綴兼容,對於那些還沒有掌握C ++ 11的開發人員更加理解。
這讓我相信一個副本會從std :: string和lvalue引用版本的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 VS std :: move以查看更多詳細信息,這可以清楚地解釋您的問題。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.