c++11 中的 move 与 forward
一. move
關于 lvaue 和 rvalue,在 c++11 以前存在一個有趣的現象:T& ?指向 lvalue (左傳引用), const T& 既可以指向 lvalue 也可以指向 rvalue。但卻沒有一種引用類型,可以限制為只指向 rvalue。這乍看起來好像也不是很大的問題,但其實不是這樣,右值引用的缺失有時嚴重限制了我們在某些情況下,寫出更高效的代碼。舉個粟子,假設我們有一個類,它包含了一些資源:
class holder {public:holder(){resource_ = new Resource();}~holder(){delete resource_;}holder(const holder& other){resource_ = new Resource(*other.resource_);}holder(holder& other){resource_ = new Resource(*other.resource_);}holder& operator=(const holder& other){delete resource_;resource_ = new Resource(*other.resource_);return *this;}holder& operator=(holder& other){delete resource_;resource_ = new Resource(*other.resource_);
return *this;}private:Resource* resource_; };
這是個 RAII 類,構造函數與析構函數分別負責資源的獲取與釋放,因此也相應處理了拷貝構造函數 (copy constructor) 和重載賦值操作符 (assignment operator)。現在假設我們這樣來使用這個類。
// 假設存在如一個函數,返回值為holder類型 holder get_holder() { return holder(); }holder h; foo(h);h = get_holder();
這小段代碼的最后一條語句做了3件事情:
1) ?銷毀 h 中的資源。
2) ?拷由 get_holder() 返回的資源。
3) ?銷毀 get_holder() 返回的資源。
我們顯然可以發現這其中做了些不是很有必要的事情,假如我們可以直接交換?h 中的資源與 get_holder() 返回的對象中的資源,那我們就可以直接省略掉第二步中的拷貝動作了。而這里之所以交換能達到相同的效果,是因為 get_holder() 返回的是臨時的變量,是個 rvalue,它的生命周期通常來說很短,具體在這里,就是賦值語句完成之后,任何人都沒法再引用該rvalue,它馬上就要被銷毀了,而如果是像下面這樣的用法,我們顯然不可以直接交換兩者的資源:
holder h1; holder h2;h1 = h2;foo(h2);
因為 h2 是個 lvalue,它的生命周期較長,在賦值語句結束之后,變量仍然存在,還有可能要被別的地方使用。因此,rvalue 的短生命周期給我們提供了在某些情況優化代碼的可能。但這種可能在 c++11 以前是沒法利用到的,因為我們沒法在代碼中對 rvalue?區別對待,在函數體中,程序員無法分辨傳進來的參數到底是不是 rvalue,缺少一個 rvalue 的標記。回憶一下,T& 指向的是 lvalue,而 const T& 指向的,卻可能是 lvalue 或 rvalue,我們沒有任何方式能夠確認當前參數是否是 rvalue!
?
為了解決這個問題,c++11 中引入了一個新的引用類型: T&&。這種引用指向的變量是個 rvalue, 有了這個引用類型,我們前面提到的問題就迎刃而解了。
class holder {public:holder(){resource_ = new Resource();}~holder(){if (resource_) delete resource_;}holder(const holder& other){resource_ = new Resource(*other.resource_);}holder(holder& other){resource_ = new Resource(*other.resource_);}holder(holder&& other){resource_ = other.resource_;other.resource_ = NULL;}holder& operator=(const holder& other){delete resource_;resource_ = new Resource(*other.resource_);return *this;}holder& operator=(holder& other){delete resource_;resource_ = new Resource(*other.resource_);
return *this;}holder& operator=(holder&& other){std::swap(resource_, other.resource_);
return *this;}private:Resource* resource_; };
這時我們再寫如下代碼的時候:
holder h1; holder h2;h1 = h2; // 調用operator(holder&); h1 = get_holder(); // 調用operator(holder&&)編譯器就能根據當前參數的類型選擇相應的函數,顯然后面的實現是更高效的。寫到里,有的人也許會有疑問: T&& ref ?指向的是右值(右值引用),那 ref 本身是左值還是右值?具體來說就是:
1 holder& operator=(holder&& other) 2 { 3 holder h = other; // 這里調用的是operator=(holder&) 還是operator=(holder&&)? 4 return *this; 5 }這個問題的本質還是怎么區分 rvalue??c++11 中對 rvalue 作了明確的定義:
Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.如果一個變量有名字,它就是 lvalue, 否則,它就是 rvalue。根據這樣的定義,上面的問題中,other 是有名字的變量,因此是個 lvalue,因此第3行調用的是 operator=(holder&).
好了說這么久,一直沒說到 move(),現在我們來給出定義:
c++11中的 move() 是這樣一個函數,它接受一個參數,然后返回一個該參數對應的右值引用.就這么簡單!你甚至可以暫時想像它的原型是這樣的(當然是錯的,正確的原型我們后面再講)
T&& move(T& val);那么,這樣一個 move() 函數,它有什么使用呢?用處大了!回到前面例子,我們用到了 std::swap() 這個函數,回想一下以前我們是怎么想來實現 swap 的呢?
1 void swap(T& a, T& b) 2 { 3 T tmp = a; 4 a = b; 5 b = tmp; 6 }想像一下,如果 T 是我們之前定義的 holder,這里面就多做了很多無用功,每一個賦值語句,就有一次資源銷毀以及一次拷貝!而事實上我們只是要交換 a 與 b 的內容,中間的拷貝都是額外的負擔,完全可以考慮消除這些無用功。
1 void swap(T& a, T& b) 2 { 3 T tmp = move(a); 4 a = move(b); 5 b = move(tmp); 6 }這樣一來,如果 holder 提供了 operator=(T&&) 重載, 上述操作就相當于只是交換了3次指針,效率大大提升!move() 使得程序員在有需要的情況下能把 lvalue 當成右值來對待。
二. forward()
1. 轉發問題
除了 move() 語義之外,右值引用的提出還解決另一個問題:完善轉發 (perfect forwarding),轉發問題主要針對的是模板函數,這些函數要處理的是這樣一個問題:假設我們有這樣一個模板函數,它的作用是:緩存一些 object,必要的時候,創建新的。
template<class TYPE, class ARG> TYPE* acquire_obj(ARG arg) {static list<TYPE*> caches;TYPE* ret;if (!caches.empty()){ret = caches.pop_back();ret->reset(arg);return ret;}ret = new TYPE(arg);return ret; }這個模板函數的作用簡單來說,就是轉發一下參數 arg 給 TYPE 的 reset() 函數和構造函數,除此它就沒再干別的事情,在這個函數當中,我們用了值傳遞的方式來傳遞參數,顯然是比較低效的,多了次沒必要的拷貝。于是我們準備改成傳遞引用的方式,同時考慮到要能接受 rvalue 作為參數,最后做出艱難的決定改成如下樣子:
template<class TYPE, class ARG> TYPE* acquire_obj(const ARG& arg) {//... }但這樣寫很不靈活:
1) 首先,如果 reset() 或 TYPE 的構造函數不接受 const 類型的引用,那上述的函數就不能使用了,必須另外提供非 const TYPE& 的版本,參數一多的話,很麻煩。
2) 其次,如果 reset( ) 或 TYPE 的構造函數能夠接受 rvalue 作為參數的話,這個特性在 acquire_obj() 里頭也永遠用不上。
其中1) 好理解,2) 是什么意思?
2) 說的是這樣的問題,即使 TYPE 存在 TYPE(TYPE&& other) 這樣的構造函數,它在 acquire_obj() 中也永遠不會被調用,原因是在 acquire_obj 中,傳遞給 TYPE 構造函數的,永遠是 lvalue,哪怕外面調用 acquire_obj() 時,用戶傳遞進來的是 rvalue,請看如下示例:
holder get_holder();holder* h = acquire_obj<holder, holder>(get_holder());雖然在上面的代碼中,我們傳遞給 acquire_obj 的是一個 rvalue,但是在 acuire_obj 內部,我們再使用這個參數時,它卻永遠是 lvalue,因為它有名字 --- 有名字的就是 lvalue.
acquire_obj 這個函數它的基本功能本來只是傳發一下參數,理想狀況下它不應該改變我們傳遞的參數的類型:假如我們傳給它 lvalue,它就應該傳 lvalue 給 TYPE,假如我們傳 rvalue 給它,它就應該傳 rvalue 給 TYPE,但上面的寫法卻沒有做到這點,而在 c++11 以前也沒法做到。forward() 函數的出現,就是為了解決這個問題。
forward() 函數的作用:它接受一個參數,然后返回該參數本來所對應的類型的引用。2. 兩個原則
C++11 引入了右值引用的符號:&&,從前面一路看下來,可能有人已經習慣了一看到 T&& 就以為這是右值引用,這確實很容易誤解,T&& ?為右值引用是當且僅當 T 為一個具體的類型時才成立,而如果T是推導類型時(如模板參數, auto)這就不一定了,比如說如下代碼中的 ref_int,它根據定義必是一個右值引用,但模板函數 func 的參數 arg 則不定是右值引用了,因為此時 T 是一個推導類型。
int&& ref_int;template <typename T> void func(T&& arg) { }Scott Meyer 曾對 T&& 這個特殊的東西專門作過一個 talk,他稱 T&& 為 universal reference(更新:不久后,c++ 社區認為叫作 forwarding reference 更準確),Universal reference 被實例化后(instantiate),即可能是一個左值引用,也可能是一個右值引用,具體來說,對于推導類型 T,??如果?T&& v? 被一個左值初始化,那 v 就是左值引用,如果 v 被右值初始化,那它就是右值引用,很神奇!這是怎么做到的呢?主要來說,在參數推導上,c++ 加入了如下兩個原則:
原則 (1):
引用折疊原則 (reference collapsing rule),注意,以下條目中的 T 為具體類型,不是推導類型。
1) ?T& & (引用的引用) 被轉化成 T&.
2)T&& & (rvalue的引用)被傳化成 T&.
3) ?T& && (引用作rvalue) 被轉化成 T&.
4) ?T&& && 被轉化成 T&&.
原則 (2):
對于以 rvalue reference 作為參數的模板函數,它的參數推導也有一個特殊的原則:
假設函數原型為:
template<class TYPE, class ARG> TYPE* acquire_obj(ARG&& arg);1) 如果我們傳遞 lvalue 給 acquire_obj(),則 ARG 就會被推導為 ARG&,因此
ARG arg; acquire_obj(arg); // 此時 acquire_obj 被推導為: TYPE* acquire_obj(ARG& &&);// 根據前面說的折疊原則,我們得到如下原型的函數。
TYPE* acquire_obj(ARG&);
2) ?如果我們傳遞 rvalue 給 acquire_obj(), ARG 就會被推導為ARG。
acquire_obj(get_arg()); // acquire_obj 被推導為 acquire_obj(ARG&&)3.結論
有了以上兩個原則,現在我們可以給出理想的 acquire_obj 的原型,以及 forward() 的原型。
template<class TYPE> TYPE&& forward(typename remove_reference<TYPE>::type& arg) {return static_cast<TYPE&&>(arg); }template<class TYPE, class ARG> TYPE* acquire_obj(ARG&& arg) {return new TYPE(forward<ARG>(arg)); }注意上面 forward 的原型,這里只給出了參數是左值引用的原型,其實還有一個接受右值引用的重載,另外就是,forward 的模板參數類型 TYPE 與該函數的參數類型并不是直接等價,因此無法根據傳入的參數確定模板參數,使用時需要調用方手動去指定模板參數的類型。
?
下面我們驗證一下,上述函數是否能正常工作,假如我們傳給 acquire_obj 一個 lvalue,根據上面說的模板推導原則,ARG 會被推導為 ARG&,我們得到如下函數:
TYPE* acquire_obj(ARG& && arg) {return new TYPE(forward<ARG&>(arg)); }以及相應的forward()函數。TYPE& && forward(typename remove_reference<TYPE&>::type& arg) {return static_cast<TYPE& &&>(arg); }再根據折疊原則,我們得到如下的函數: TYPE* acquire_obj(ARG& arg) {return new TYPE(forward<ARG&>(arg)); }以及相應的forward()函數。TYPE& forward(typename remove_reference<TYPE&>::type& arg) {return static_cast<TYPE&>(arg); }?所以,最后在 acquire_obj 中,forward 返回了一個 lvalue 引用, TYPE 的構造函數接受了一個 lvaue 引用, 這正是我們所想要的。
?而假如我們傳遞給 acquire_obj 一個 rvalue 的參數,根據模板推導原則,我們知道 ARG 會被推導為 ARG,于是得到如下函數:?
TYPE* acquire_obj(ARG&& arg) {return new TYPE(forward<ARG>(arg)); }以及相應的 forward() 函數。TYPE&& forward(typename remove_reference<TYPE>::type& arg) {return static_cast<TYPE&&>(arg); }最后 acquire_obj 中 forward() 返回了一個 rvalue,TYPE 的構造函數接受了一個 rvalue,也是我們所想要的。可見,上面的設計完成了我們所想要的功能,這時的 acquire_obj 函數才是完美的轉發函數。
三.move的原型
顯然,move() 必是一個模板函數,它的參數類型推導完全遵循前面提到兩個原則,這就是為何我把它的原型放到現在才寫出來,用心良苦啊。
template<class T> typename remove_reference<T>::type&& std::move(T&& a) {typedef typename remove_reference<T>::type&& RvalRef;return static_cast<RvalRef>(a); }根據模板推導原則和折疊原則,我們很容易驗證,無論是給 move 傳遞了一個 lvalue 還是 rvalue,最終返回的,都是一個rvalue reference。而這正是 move 的意義,得到一個 rvalue 的引用。
?
看到這里有人也許會發現,其實就是一個cast 嘛,確實是這樣,直接用 static_cast 也是能達到同樣的效果,只是 move 更具語義罷了。
轉載于:https://www.cnblogs.com/lvdongjie/p/4489776.html
總結
以上是生活随笔為你收集整理的c++11 中的 move 与 forward的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 无法识别的属性“targetFramew
- 下一篇: ZOJ3865:Superbot(BFS