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
void setResource(std::shared_ptr
void setResource(std::shared_ptr
這東西有辦法編譯?相信大多數人看到的第一個感想會是這樣,但神奇的是編譯器不但能編譯而且還正常運行著其產生的版本。當然,若是你試著用不是template的方式去將function寫出來,編譯器馬上會叫錯誤給你看。其實這是為了解決template可以帶入任何參數的問題,而由編譯器所導入的一個機制:Reference Collapsing。它的規則如下:
- **
A& &becomesA&** **A& &&becomesA&** **A&& &becomesA&**A&& &&becomesA&&
所以在這裡的例子,我們的void setResource(std::shared_ptr
//perfect forwarding
{
URWidget widget;
widget.setResource<std::shared_ptr
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上下載:網址在此