程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> c++11中的move與forward

c++11中的move與forward

編輯:C++入門知識

一. 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更具語義罷了。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved