簡體   English   中英

我應該通過右值參考返回rvalue參考參數嗎?

[英]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的問題,並得出結論這是不好的做法。

返回對參數的引用會使調用者的合同強制轉向

  1. 參數不能是臨時的(這正是rvalue引用所代表的),或者
  2. 返回值將不會保留在調用者上下文中的下一個分號之后(當臨時值被銷毀時)。

如果調用者傳遞臨時值並嘗試保存結果,則會獲得懸空引用。

根據我的閱讀,似乎共識是因為返回值是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一些(非代表性)運行時:

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

如果你的問題是純粹的優化導向,最好不要擔心如何傳遞或返回一個參數。 編譯器非常智能,可以將代碼擴展為純引用傳遞,復制省略,函數內聯,甚至移動語義,如果它是最快的方法。

基本上,移動語義可以在一些深奧的案例中受益。 讓我們說我有保持矩陣對象double**作為成員變量和該指針指向的兩個dimenssional陣列double 現在讓我說我有這個表達式:
Matrix a = b+c;
一個拷貝構造(或分配新建分配FY操作者,在這種情況下)將得到的總和bc作為temorary,它傳遞為const參考,重新分配m*n的量doublesa內的指針,然后,它將運行上a+b sum-array,將逐個復制其值。 簡單的計算表明,它可能需要達到O(nm)步(可以通過O(n^2) )。 移動語義只會將那個隱藏的double**重新連接到a內部指針。 需要O(1)
現在讓我們暫時考慮std::string :將它作為引用傳遞需要O(1)步(獲取內存地址,傳遞它,取消引用等等,這在任何類型都不是線性的)。 將它作為r值引用傳遞需要程序將其作為引用傳遞,重新連接隱藏的底層C- char* ,它保存內部緩沖區,使原始緩沖區為空(或在它們之間交換),復制sizecapacity以及更多的行動。 我們可以看到,雖然我們仍然在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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM