Universal,這個字在其實是一種抽象的表達。任何抽象的表達,在下面大多會有一套自動化的機制,來將本該由使用者操作的複雜概念隱藏起來。Universal References可以說是一種抽象的介面,它提供了一個統一的窗口給lvalue reference以及rvalue reference。但是從上面幾篇文章我們不是才知道,將這二個reference的概念區分出來就是為了要減少暫存物件記憶體操作的成本嗎?現在說要把它們又合在一起不是本末倒置了嗎?──當然不會,這裡所說提供的統一窗口,是為了給在設計功能時,還不知道傳入物件型態的,需要在編譯期推導出來的情況下使用。
想想看,在什麼情況下會有這種情形?其實這種實作在標準函式中到處都是。應該呼之欲出了吧?答案是template。
template又稱為編譯期靜態多型,跟使用繼承方式的執型期動態多型的方式不同,它會在我們按下編譯的時候才將需要的程式碼產生出來。雖然介面上看起來只有一個,但實際上它在底下可是產生了lvalue reference跟rvalue reference二個版本。這種性質可以說是一種拯救;它讓我們程序猿們省下了不小維護程式碼的心力,因為,這樣我們就不需要對每個member variable製作二個不同的版本的setter。例如,若沒使用universal reference的程式碼會是下面的情況:
class Widget { public: Widget(){}; void setName(const std::string& newName) { m_name = newName; } // const lvalue void setName(std::string&& newName) { m_name = std::move(newName); } // rvalue void setResource(std::shared_ptr<Resource>& resourcePtr){ m_resourcePtr = resourcePtr; } void setResource(std::shared_ptr<Resource>&& resourcePtr){ m_resourcePtr = std::move(resourcePtr); } private: std::string m_name; std::shared_ptr<Resource> m_resourcePtr; }; int main(){ { Widget widget; widget.setName("rvalue reference"); std::string name = "lvalue reference"; widget.setName(name); widget.setResource(std::make_shared<Resource>()); auto resourcePtr = std::make_shared<Resource>(); widget.setResource(resourcePtr); } return 0; }
用了之後馬上減少2個方法的實作:
class URWidget{ public: URWidget(){}; template<typename T> void setResource(T&& newResoure) // universal reference { //name = std::move(newName); //bad m_resourcePtr = std::forward<T>(newResoure); } template<typename T> void setName(T&& newName) // universal reference { m_name = std::forward<T>(newName); } private: std::string m_name; std::shared_ptr<Resource> m_resourcePtr; }; int main(){ { URWidget widget; widget.setName("rvalue reference"); std::string name = "lvalue reference"; widget.setName(name); widget.setResource(std::make_shared<Resource>()); auto resourcePtr = std::make_shared<Resource>(); widget.setResource(resourcePtr); } return 0; }
這麼好的性質,難道不用嗎?由於Universal Reference傳入的參數可以是任何版本的reference,所以要特別注意到的是我們必須包上std::forward這一層:若傳入的是lvalue,則呼叫lvalue reference版本;若傳入的是rvalue,則呼叫rvalue reference版本。有人可能會問,為什麼我在上面的code中把std::move註解掉了,並打上一個bad。這是因為,若傳入的是rvalue則一切運行如常,但若傳入的是lvalue,則可能會造成使用的時候再次呼叫該物件而造成程式崩潰。例如,當我們在呼叫完widget.setResource(resourcePtr)之後,再次去呼叫resourcePtr的情況。若是這裡是真的想要把resourcePtr轉移給widget的話,則必須要再包上一層std::move表明:widget.setResource(std::move(resourcePtr))。
另外必須要注意的是,若用下面這種方式寫的話,會在傳入lvalue的時候發生編譯錯誤:
template<typename T> class TWidget{ public: TWidget(){}; void setResource(T&& newResoure) { //name = std::move(newName); //bad m_resourcePtr = std::forward<T>(newResoure); } void setName(T&& newName) { m_name = std::forward<T>(newName); } private: std::string m_name; std::shared_ptr<Resource> m_resourcePtr; }; int main(){ { TWidget<std::shared_ptr<Resource>> widget; widget.setResource(std::make_shared<Resource>()); auto resourcePtr = std::make_shared<Resource>(); widget.setResource(resourcePtr);//compile error, rvalue reference version can't bind lvalue } return 0; }
這是因為setResource的版本在widget建出來的時候就已經產生完畢,在這裡的例子由於只有產生rvalue的版本,因此在呼叫lvalue的時候就會編譯錯誤。讓我們再多做一些實驗看看會怎麼樣:
int main(){ { TWidget<const std::shared_ptr<Resource>&> widget; widget.setResource(std::make_shared<Resource>());//will call lvalue reference version auto resourcePtr = std::make_shared<Resource>(); widget.setResource(resourcePtr); //will call lvalue reference version } { TWidget<std::shared_ptr<Resource>&&> widget; widget.setResource(std::make_shared<Resource>()); auto resourcePtr = std::make_shared<Resource>(); // widget.setResource(resourcePtr); //compile error, rvalue reference version can't bind lvalue } return 0; }
結果很奇妙,為什麼TWidget<std::shared_ptr<Resource>&> widget; 跟TWidget<std::shared_ptr<Resource>&&> widget;能夠編譯,而且產生的setResource的版本還不一樣。在試著將template的參數帶入TWidget之後,我們會發現setResource變成很奇怪的形態:
void setResource(std::shared_ptr<Resource>& && newResoure) void setResource(std::shared_ptr<Resource>&& && newResoure)
這東西有辦法編譯?相信大多數人看到的第一個感想會是這樣,但神奇的是編譯器不但能編譯而且還正常運行著其產生的版本。當然,若是你試著用不是template的方式去將function寫出來,編譯器馬上會叫錯誤給你看。其實這是為了解決template可以帶入任何參數的問題,而由編譯器所導入的一個機制:Reference Collapsing。它的規則如下:
A& &
becomesA&
A& &&
becomesA&
A&& &
becomesA&
A&& &&
becomesA&&
所以在這裡的例子,我們的void setResource(std::shared_ptr<Resource>& && newResoure) 變成了void setResource(std::shared_ptr<Resource>& newResoure) ;setResource(std::shared_ptr<Resource>&& && newResoure) 則變成了setResource(std::shared_ptr<Resource>&& newResoure) 。看吧,一切又變成了如往常的美好。也就是因為這個機制,才能讓Universal Reference不受template帶入參數的影響,完成Perfect Forwarding:
//perfect forwarding { URWidget widget; widget.setResource<std::shared_ptr<Resource> >(std::make_shared<Resource>());//call rvalue version auto resourcePtr = std::make_shared<Resource>(); widget.setResource<std::shared_ptr<Resource>& >(resourcePtr); //call lvalue version widget.setResource<std::shared_ptr<Resource>&& >(std::move(resourcePtr));//call rvalue version }
最後,在明白了move語句的強大之處後,或許有人會想要這樣應用:
static URWidget makeWidget() // Moving version of makeWidget { URWidget w; //do somthing return std::move(w); // move w into return value } // (don't do this!)
嗯,直接把local object轉移給回傳的暫存物件,挺好的不是嗎?看起來是很理想,但由於編譯器有Return Value Optimization及NRVO的機制,因此若回傳值被包上任何function的話,這個機制就不會被啟動。雖然move還蠻快的,但速度上還是比不上RVO,因此不要在RVO機制啟動的情況下包上一層std::move,因為速度不會變快。當然,還是會有需要的情況,但這通常是因為RVO啟動的條件沒達成。
最後,本系列的程式碼,可在github上下載:網址在此
Anonymous
setName使用
void setName(const std::string& newName)
void setName(std::string&& newName)
會比你使用
template
void setName(T&& newName)
還要好
如果使用template版本,表示的是”任意型態”皆可以用來setName
dorgon
其實這就是設計上的選擇
有時候過度設計並不是好事,
因為很有可能會造成語意上理解的困難
很多時候是看專案實際上的需求跟maintain的人來決定實作的方式。
老實說我並不會想要在跟人合作的專案中導入這裡提到的概念…
因為大部份的人都對C++11這些特性還不熟悉。
這邊的程式是展示概念的用途居多: )