標籤:

C++的右值引用有坑嗎?

我了解右值引用的過程:

左值/右值-&>C++11特性-&>完美轉發

1.一個問題是universal reference(即T)和std::forward()效果是都能完美轉發?

using namespace std;

class hehe{
public:
hehe():m_a(0),m_b(1) {
cout &<&< "ctor" &<&< endl; } ~hehe(){ cout &<&< "dtor" &<&< endl; } int m_a; int m_b; }; template&
void foo(T a)
{
}

hehe hh()
{
return hehe();
}

template&
void fwd(T a)
{
foo(a);
}

int main(){
hehe a;
fwd(a);
fwd(hh());

return 0;
}

和這種有什麼區別?

template&
void fwd(T a)
{
foo(forward&(a));
}

2.有篇blog:C++11:深入理解右值引用,move語義和完美轉發 - 代碼妖嬈 - 博客頻道 - CSDN.NET

最後說「完美轉發其實不完美」,難道還有什麼坑??

3.生產環境中都怎麼用右值引用、forward這些?


首先,T == T,T = T。其次,你不用forward,有些時候會編譯失敗,譬如說:

template&
void foo(T a)
{
}

template&
void fwd(T a)
{
foo&(a); // 這裡的寫法跟題目不同,強行制定foo跟fwd要有相同的T。
}

int main()
{
fwd&(1);
return 0;
}

forward的意思就是,當T==T的時候返回T,當T==T的時候返回T。當然給進去的參數,必須是T,所以他實際上是在T 和T 裡面挑。這所有的東西:

  • T a

  • forward&(a)

  • foo&(...)

三個必須一起出現,才構成完美轉發。我也覺得這種搞法很蛋疼,但是他有用啊,這樣就行了


一、

分清並區別對待universal reference和rvalue reference。

在函數參數表裡出現T類型,第一件事就是分清楚這東西究竟是右值引用還是通用引用(在Effective Modern C++里Meyers將其稱為Universal Reference)。區別就是看T是編譯器推導得到的,還是固定的。

確定這一點之後將右值引用和通用引用在腦內當作兩種東西分別處理。

簡而言之,

template&
void barbar(T t);

裡面的T從最開始就不要把它當作右值引用。

其他情況,比如這種

template&
class Foo {
void foo(T t); // 對foo()而言,T是一個固定的類型,雖然對Foo而言它是個模板參數
}

這裡面的T就是一個rvalue reference。

然後universal reference配合std::forward來實際操作完美轉發,rvalue reference配合std::move進一步使用該右值引用。

二、

那個博文結尾提的那段,與其說是踩了perfect forwarding的坑,倒不如說是踩了uniform initialization的坑。因為在調用fwd的時候編譯器不會根據fwd的函數體裡面怎麼使用參數去推導fwd的模版參數T,只會根據fwd被調用時傳入的參數來推,而這個時候沒人知道後面還貓著個vector,於是編譯器不知道你花括弧是想初始化啥玩意。這不是perfect forwarding的鍋,不管你per不perfect,花括弧初始化列表都沒法這麼forward進去。所以這裡編譯器一般應該報錯推導不出類型T,翻譯成東北話就是「我卻我滋道你想粗絲化個蛤呀」。

想要實現forward「一個花括弧」進去,需要fwd(std::initializer_list&({1,2,3}));這樣。

另外提一下,uniform initialization是真的有坑,不光是這一個坑,例子這個還好,涉及構造函數重載的時候事更多。用時需謹慎。咱一般不用,頂多用來初始化個STL容器。

三、

有一種方式是在代碼風格中全部禁止,理由是「右值引用學習起來複雜,弄不好有坑」。典型的思維懶惰外加守舊心態,核心原因就是自己懶得學而已。跟禁用STL差不多。

除了這種極端情況以外大概就是各有己見了吧。咱一般在生產環境里主要用在二點,一是避免對STL容器以及自定義的、移動構造有意義的類型的不必要複製;二是用於不可複製但是可以轉移內部資源的類型,比如unique_ptr、thread這種,包括標準庫里已有的以及自己寫的。


1、2:個人認為,右值引用最大的坑,就是你並不能總是以非常低的精神力代價,正確地判斷丫什麼時候被識別為右值引用。這會在調試時帶來額外的精神負載。

3:由於上面那點,對於一些非容器性質的業務類,我經常會只開啟移動語義或者複製語義其中之一。這是因為我們現在的項目里,那些業務類絕大部分都只需要移動或者複製,而不需要同時做兩者。


我用的時候感覺一個比較大的坑是,不是任何時候使用傳值都能保證不發生拷貝。

比如:

std::string foo() {
std::string bar = "baz";
return bar;
}

int main() {
auto s = foo();
}

這時候,會發生 copy elision,因為編譯器判斷 foo 內的 bar 可以安全被移入 s。但如果是這樣:

void foo(std::string s) {
std::string bar = s;
}

int main() {
std::string s = "baz";
foo(s);
}

這時候,即使 main 中的 s 在調用 foo(s) 之後再也不被使用到了,編譯器也不會判斷 s 能被安全移入 foo,所以會發生拷貝!

結論是,只有 prvalue 和 xvalue 確定會被編譯器認為能安全地移動。

所以不要任何時候都傳值。當你只需要讀取對象時,使用 const 引用:

void foo(const std::string s) {
std::cout &<&< s &<&< " "; }

當你確實需要獲得對象的拷貝,有時候你需要手動地 move 來避免拷貝:

struct A {
A(std::string s) : str(std::move(s)) {}
std::string str;
};

int main() {
std::string s = "baz";
auto a = A(std::move(s));
}

這增加了很多工作量和需要思考的細節。。。(我彷彿看見 Rust 在向我招手


這個東西本來就是填坑用的,遇到坑最好是避開,而不是填坑。引用本身就是坑,慎入


右值引用自己本身就是個坑,如這個樓里的一個回答,你並不能毫無精神負擔的使用它


對性能的追去從來沒個止境 避免發生任何不必要的拷貝的前提就是先把自己腦子燒出個坑


函數聲明的參數類型是針對傳入的實際參數的說明。右值引用參數表示這個參數可以接收左值或右值,並且在函數內部還可以修改它。
但是,在函數內部,函數的形式參數總是左值!函數內部可以對聲明為右值引用的參數取地址,而C++是不允許對右值取地址的。所以當在函數要用這個參數去調用另一個函數時,如果這個被調用的函數恰好也接收一個右值引用參數,就必須用forward做完美轉發。

我不引進 universal reference 的說法,這樣只會讓想法概念更複雜。


推薦閱讀:

C++ new分配的內存不delete會泄漏嗎?
C 與 C++ 的真正區別在哪裡?
C語言的宏定義和C++的內聯函數有什麼意義?
看見網上說學單片機有助於c++的學習,是這樣的嗎?
看完 C++ 課本能否直接上手 Qt?

TAG:C |