一. 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(); holder 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; 因為h2是個lvalue,它的生命周期較長,在賦值語句結束之後,變量還要存在,還有可能要被別的地方使用。 顯然,rvalue的短生命周期給我們提供了在某些情況優化代碼的可能。 但這種可能在c++11以前是沒法利用到的,因為:我們沒法在代碼中對rvalue區別對待,在函數體中,無法分辨傳進來的參數到底是不是rvalue,缺少一個rvalue的標記。 回憶一下 T& 指向的是lvalue,而const T&指向的,卻可能是lvalue或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()是這樣一個函數,它接受一個參數,然後返回一個該參數對應的rvalue(). 就這麼簡單!你甚至可以暫時想像它的原型是這樣的(當然是錯的,正確的原型我們後面再講) 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,這裡面多做了多少無用功啊,每一個賦值語句,就有一次資源銷毀,以及一次拷貝!但如果用上了move(). 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當成rvalue來使用。 二. forward() 1.轉發問題 除了move()語義之外,rvalue的提出還為了解決另一個問題:轉發(forward). 假設我們有這樣一個模板函數,它的作用是:緩存一些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,因為它有名字。 acquire_obj這個函數它的基本功能只是傳發一下參數,理想狀況下它不應該改變我們傳遞參數的類型:假如我們傳給它lvalue,它就應該傳lvalue給TYPE,假如我們傳rvalue給它,它就應該傳rvalue給TYPE,但上面的寫法卻沒有做到這點,而在c++11以前也沒法做到。 forward()函數的出現,就是為了解決這個問題。 forward()函數的作用:它接受一個參數,然後返回該參數本來所對應的類型。 比如說在上述的例子中(暫時省略參數的原型,後面再介紹): 復制代碼 holder* h = acquire_obj<holder, holder>(get_holder()); //假設 acquire_obj()接受了一個rvalue作為參數,在它的內部, TYPE* acquire_obj(arg) { //arg本來是rvalue,如果我們直接引用,它會被當成lvalue來使用。 //但如果我們用forward()處理一下,我們卻可以得到它的rvalue版本。 //此處 TYPE的構造函數接受的是一個rvalue。 TYPE* ret = new TYPE(forward(arg)); } //但如果我們傳給acquire_obj()的是一個lvalue, holder h1; //acquire_obj接受了lvalue作為參數。 acquire_obj<holder,holder>(h1); TYPE* acquire_obj(arg) { //此處,TYPE的構造函數接受的是一個lvalue。 TYPE* ret = new TYPE(forward(arg)); } 復制代碼 2. 二個原則 要理解forward()是怎麼實現的,先得說說c++11中關於引用的二個原則。 原則(1): 引用折疊原則(reference collapsing rule) 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被推導為 acquire_obj(ARG& &&) 根據前面說的折疊原則,acquire_obj(ARG& &&) 最後變成 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)); } 復制代碼 下面我們驗證一下,上述函數是否能正常工作,假如我們傳給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的原型 復制代碼 template<class T> typename remove_reference<T>::type&& std::move(T&& a) { typedef typename remove_reference<T>::type&& RvalRef; return static_cast<RvalRef>(a); } 復制代碼 根據rvalue引用的模板推導原則和折疊原則,我們很容易驗證,無論是給move傳遞了一個lvalue還是rvalue,最終返回的,都是一個rvalue reference. 而這正是move的意義,得到一個rvalue的引用。 看到這裡有人也許會發現,其實就是一個cast嘛,確實是這樣,直接用static_cast也是能達到同樣的效果,只是move更具語義罷了。