2015-07-11 96 views
5

幫手如果你看get,輔助功能爲std::tuple,你會發現以下過載:爲什麼得到的std ::的元組返回右值引用,而不是值

template< std::size_t I, class... Types > 
constexpr std::tuple_element_t<I, tuple<Types...> >&& 
get(tuple<Types...>&& t); 

換句話說,它返回當輸入元組是右值引用本身時的右值引用。爲什麼不按價值回報,在函數體中調用move?我的觀點如下:get的返回值將被綁定到一個引用或者一個值(它可以被綁定到我想象的任何東西,但這不應該是一個常見的用例)。如果它被綁定到一個值,那麼無論如何都會發生移動建設。所以你不會因爲回報而損失任何東西。如果你綁定到一個引用,那麼返回一個右值引用實際上可能是不安全的。爲了顯示一個例子:

struct Hello { 
    Hello() { 
    std::cerr << "Constructed at : " << this << std::endl; 
    } 

    ~Hello() { 
    std::cerr << "Destructed at : " << this << std::endl; 
    } 

    double m_double; 
}; 

struct foo { 
    Hello m_hello; 
    Hello && get() && { return std::move(m_hello); } 
}; 

int main() { 
    const Hello & x = foo().get(); 
    std::cerr << x.m_double; 
} 

當運行時,該程序打印:

Constructed at : 0x7ffc0e12cdc0 
Destructed at : 0x7ffc0e12cdc0 
0 

換言之,X是立即懸空參考。而如果你只是這樣寫foo:

struct foo { 
    Hello m_hello; 
    Hello get() && { return std::move(m_hello); } 
}; 

這個問題不會發生。此外,如果你再使用FOO這樣的:

Hello x(foo().get()); 

它似乎並不像有任何額外的開銷是否通過值或右值引用返回。我測試過這樣的代碼,看起來它會一直只執行一次移動構建。例如。如果我添加一個成員:

Hello(Hello &&) { std::cerr << "Moved" << std::endl; } 

我構建X如上,我的程序只「感動」一次不管我是否通過值或右值引用返回打印。

有沒有很好的理由我錯過了,或者這是一個疏忽?

注:這裏有一個很好的相關問題:Return value or rvalue reference?。似乎認爲在這種情況下價值回報通常是可取的,但STL出現的事實讓我很好奇STL是否忽略了這種推理,或者如果他們有特殊的理由可能不適用通常。

編輯:有人提出這個問題是Is there any case where a return of a RValue Reference (&&) is useful?的重複。不是這種情況;這個答案建議通過右值引用返回,作爲避免數據成員複製的一種方式。正如我上面詳細討論的那樣,如果您首先撥打move,那麼無論您是按價值還是按右值參考,複製都將被忽略。

+0

這不是遠程重複。我的答案明確而詳細地討論瞭如何通過值返回並事先調用std :: move實現與從數據成員移動相同的效果。我的問題與兩種方法的差異有關。我引用自己的問題與你引用的問題有更多的關係。在急於標註重複的內容之前,請仔細閱讀。 –

+0

如果您按值返回 - 您創建一個副本。如果您移動它,則移動副本。如果你返回一個r值引用,你可以移動它,然後移動原始對象,或者不用移動字體處理它,那麼它的行爲就像常規引用。問題是如果你返回一個r值參考,你不會強制複製。 –

+0

爲什麼需要移動可施工性? – Columbo

回答

4

你可以用這個方法來創建一個懸空引用的例子非常有趣,但從例子中學習正確的教訓很重要。

考慮一個非常簡單的例子,不具有任何地方任何&&

const int &x = vector<int>(1) .front(); 

.front()返回&引用新構建的載體的第一要素。這個矢量當然會被立即銷燬,並且你留下了一個懸掛的參考。

需要了解的經驗是,使用const參照通常不會延長使用期限。它延長了非參考文獻的使用期限。如果=的右側是參考,那麼你必須自己負責生命週期。

這一直是這種情況,所以tuple::get做任何不同的事情都沒有意義。 tuple::get被允許返回一個引用,就像vector::front一直一樣。

你談論移動和複製構造函數和速度。最快的解決方案是不使用任何構造函數。想象一下,一個功能來連接兩個載體:

vector<int> concat(const vector<int> &l_, const vector<int> &r) { 
    vector<int> l(l_); 
    l.insert(l.end(), r.cbegin(), r.cend()); 
    return l; 
} 

這將使優化的額外的超負荷:

vector<int>&& concat(vector<int>&& l, const vector<int> &r) { 
    l.insert(l.end(), r.cbegin(), r.cend()); 
    return l; 
} 

這種優化保持結構的數量降到最低

vector<int> a{1,2,3}; 
    vector<int> b{3,4,5}; 
    vector<int> c = concat(
    concat(
     concat(
      concat(vector<int>(), a) 
     , b) 
     , a 
    , b); 

最後一行,有四個電話concat,將只有兩個結構:起始值(vector<int>())和移動結構到c。你可以有100個嵌套調用concat那裏,沒有任何額外的結構。

所以,通過&&返回可以更快。因爲,是的,移動速度比複製速度快,但如果可以避免這兩種情況,速度還會更快。

總之,它是爲了速度而完成的。考慮在一個元組內的元組中使用一個嵌套的get系列。此外,它允許它使用既沒有複製也沒有移動構造函數的類型。

這並沒有引入任何有關生命的新風險。 vector<int>().front()「問題」不是一個新問題。

+2

我需要花更多時間與你答案充分吸收它。但請注意,具有諷刺意味的是,如果存在重載T front()&&,那麼vector.front的示例將不會很慷慨。 –

+2

我想過提及這一點。是的,委員會應該在整個標準中加入'&'和'&&',就像你在評論中的例子。但我想這會破壞兼容性。另外,也許所有的方法默認都是'&',但這對於語言來說也是一個突破性的重大改變。 –

+0

概括地說,如果一個函數返回一個參數或一個參數的子對象,並且該參數是'&&',那麼我認爲沒有理由不總是通過'&&'返回。我在這裏太過分了嗎? –

相關問題