C++0x標(biāo)準(zhǔn)出來很長(zhǎng)時(shí)間了,引入了很多牛逼的特性[1]。其中一個(gè)便是右值引用,Thomas Becker的文章[2]很全面的介紹了這個(gè)特性,讀后有如醍醐灌頂,翻譯在此以便深入理解。
右值引用是由C++0x標(biāo)準(zhǔn)引入c++的一個(gè)令人難以捉摸的特性。我曾偶爾聽到過有c++領(lǐng)域的大牛這么說:
每次我想抓住右值引用的時(shí)候,它總能從我手里跑掉。想把右值引用裝進(jìn)腦袋實(shí)在太難了。我不得不教別人右值引用,這太可怕了。
右值引用惡心的地方在于,當(dāng)你看到它的時(shí)候根本不知道它的存在有什么意義,它是用來解決什么問題的。所以我不會(huì)馬上介紹什么是右值引用。更好的方式是從它將解決的問題入手,然后講述右值引用是如何解決這些問題的。這樣,右值引用的定義才會(huì)看起來合理和自然。
右值引用至少解決了這兩個(gè)問題:
如果你不懂這兩個(gè)問題,別擔(dān)心,后面會(huì)詳細(xì)地介紹。我們會(huì)從move語義開始,但在開始之前要首先讓你回憶起c++的左值和右值是什么。關(guān)于左值和右值我很難給出一個(gè)嚴(yán)密的定義,不過下面的解釋已經(jīng)足以讓你明白什么是左值和右值。
在c語言發(fā)展的較早時(shí)期,左值和右值的定義是這樣的:左值是一個(gè)可以出現(xiàn)在賦值運(yùn)算符的左邊或者右邊的表達(dá)式e,而右值則是只能出現(xiàn)在右邊的表達(dá)式。例如:
int a = 42; int b = 43; // a與b都是左值 a = b; // ok b = a; // ok a = a * b; // ok // a * b是右值: int c = a * b; // ok, 右值在等號(hào)右邊a * b = 42; // 錯(cuò)誤,右值在等號(hào)左邊
在c++中,我們?nèi)匀豢梢杂眠@個(gè)直觀的辦法來區(qū)分左值和右值。不過,c++中的用戶自定義類型引入了關(guān)于可變性和可賦值性的微妙變化,這會(huì)讓這個(gè)方法變的不那么地正確。我們沒有必要繼續(xù)深究下去,這里還有另外一種定義可以讓你很好的處理關(guān)于右值的問題:左值是一個(gè)指向某內(nèi)存空間的表達(dá)式,并且我們可以用&操作符獲得該內(nèi)存空間的地址。右值就是非左值的表達(dá)式。例如:
// 左值: // int i = 42; i = 43; // ok, i是左值int* p = &i; // ok, i是左值int& foo(); foo() = 42; // ok, foo()是左值int* p1 = &foo(); // ok, foo()是左值// 右值: // int foobar(); int j = 0; j = foobar(); // ok, foobar()是右值int* p2 = &foobar(); // 錯(cuò)誤,不能取右值的地址j = 42; // ok, 42是右值
如果你對(duì)左值和右值的嚴(yán)密的定義有興趣的話,可以看下Mikael Kilpel?inen的文章[3]。
假設(shè)class X包含一個(gè)指向某資源的指針或句柄m_pResource。這里的資源指的是任何需要耗費(fèi)一定的時(shí)間去構(gòu)造、復(fù)制和銷毀的東西,比如說以動(dòng)態(tài)數(shù)組的形式管理一系列的元素的std::vector。邏輯上而言X的賦值操作符應(yīng)該像下面這樣:
X& X::operator=(X const & rhs){ // [...] // 銷毀m_pResource指向的資源 // 復(fù)制rhs.m_pResource所指的資源,并使m_pResource指向它 // [...]}
同樣X的拷貝構(gòu)造函數(shù)也是這樣。假設(shè)我們這樣來用X:
X foo(); // foo是一個(gè)返回值為X的函數(shù)X x;x = foo();
最后一行有如下的操作:
上面的過程是可行的,但是更有效率的辦法是直接交換x和臨時(shí)對(duì)象中的資源指針,然后讓臨時(shí)對(duì)象的析構(gòu)函數(shù)去銷毀x原來擁有的資源。換句話說,當(dāng)賦值操作符的右邊是右值的時(shí)候,我們希望賦值操作符被定義成下面這樣:
// [...]// swap m_pResource and rhs.m_pResource// [...]
這就是所謂的move語義。在之前的c++中,這樣的行為是很難實(shí)現(xiàn)的。雖然我也聽到有的人說他們可以用模版元編程來實(shí)現(xiàn),但是我還從來沒有遇到過能給我解釋清楚如何具體實(shí)現(xiàn)的人。所以這一定是相當(dāng)復(fù)雜的。C++0x通過重載的辦法來實(shí)現(xiàn):
X& X::operator=(<mystery type> rhs){ // [...] // swap this->m_pResource and rhs.m_pResource // [...] }
既然我們是要重載賦值運(yùn)算符,那么
把上面的
如果X是一種類型,那么X&&就叫做X的右值引用。為了更好的區(qū)分兩,普通引用現(xiàn)在被稱為左值引用。
右值引用和左值引用的行為差不多,但是有幾點(diǎn)不同,最重要的就是函數(shù)重載時(shí)左值使用左值引用的版本,右值使用右值引用的版本:
void foo(X& x); // 左值引用重載void foo(X&& x); // 右值引用重載X x;X foobar();foo(x); // 參數(shù)是左值,調(diào)用foo(X&)foo(foobar()); // 參數(shù)是右值,調(diào)用foo(X&&)
重點(diǎn)在于:
右值引用允許函數(shù)在編譯期根據(jù)參數(shù)是左值還是右值來建立分支。
理論上確實(shí)可以用這種方式重載任何函數(shù),但是絕大多數(shù)情況下這樣的重載只出現(xiàn)在拷貝構(gòu)造函數(shù)和賦值運(yùn)算符中,以用來實(shí)現(xiàn)move語義:
X& X::operator=(X const & rhs); // classical implementationX& X::operator=(X&& rhs){ // Move semantics: exchange content between this and rhs return *this;}
實(shí)現(xiàn)針對(duì)右值引用重載的拷貝構(gòu)造函數(shù)與上面類似。
如果你實(shí)現(xiàn)了void foo(X&);
,但是沒有實(shí)現(xiàn)void foo(X&&)
;,那么和以前一樣foo的參數(shù)只能是左值。如果實(shí)現(xiàn)了void foo(X const &)
;,但是沒有實(shí)現(xiàn)void foo(X&&)
;,仍和以前一樣,foo的參數(shù)既可以是左值也可以是右值。唯一能夠區(qū)分左值和右值的辦法就是實(shí)現(xiàn)void foo(X&&);
。最后,如果只實(shí)現(xiàn)了實(shí)現(xiàn)void foo(X&&);
,但卻沒有實(shí)現(xiàn)void foo(X&);
和void foo(X const &);
,那么foo的參數(shù)將只能是右值。
c++的第一版修正案里有這樣一句話:“C++標(biāo)準(zhǔn)委員會(huì)不應(yīng)該制定一條阻止程序員拿起槍朝自己的腳丫子開火的規(guī)則?!眹?yán)肅點(diǎn)說就是c++應(yīng)該給程序員更多控制的權(quán)利,而不是擅自糾正他們的疏忽。于是,按照這種思想,C++0x中既可以在右值上使用move語義,也可以在左值上使用,標(biāo)準(zhǔn)程序庫中的函數(shù)swap就是一個(gè)很好的例子。這里假設(shè)X就是前面我們已經(jīng)重載右值引用以實(shí)現(xiàn)move語義的那個(gè)類。
template<class T>void swap(T& a, T& b){ T tmp(a); a = b; b = tmp;}X a, b;swap(a, b);
上面的代碼中沒有右值,所以沒有使用move語義。但move語義用在這里最合適不過了:當(dāng)一個(gè)變量(a)作為拷貝構(gòu)造函數(shù)或者賦值的來源時(shí),這個(gè)變量要么就是以后都不會(huì)再使用,要么就是作為賦值操作的目標(biāo)(a = b)。
C++11中的標(biāo)準(zhǔn)庫函數(shù)std::move可以解決我們的問題。這個(gè)函數(shù)只會(huì)做一件事:把它的參數(shù)轉(zhuǎn)換為一個(gè)右值并且返回。C++11中的swap函數(shù)是這樣的:
template<class T>void swap(T& a, T& b){ T tmp(std::move(a)); a = std::move(b); b = std::move(tmp);}X a, b;swap(a, b);
現(xiàn)在的swap使用了move語義。值得注意的是對(duì)那些沒有實(shí)現(xiàn)move語義的類型來說(沒有針對(duì)右值引用重載拷貝構(gòu)造函數(shù)和賦值操作符),新的swap仍然和舊的一樣。
std::move是個(gè)很簡(jiǎn)單的函數(shù),不過現(xiàn)在我還不能將它的實(shí)現(xiàn)展現(xiàn)給你,后面再詳細(xì)說明。
像上面的swap函數(shù)一樣,盡可能的使用std::move會(huì)給我們帶來以下好處:
假設(shè)有以下代碼:
void foo(X&& x){ X anotherX = x; // ...}
現(xiàn)在考慮一個(gè)有趣的問題:在foo函數(shù)內(nèi),哪個(gè)版本的X拷貝構(gòu)造函數(shù)會(huì)被調(diào)用呢?這里的x是右值引用類型。把x也當(dāng)作右值來處理看起來貌似是正確的,也就是調(diào)用這個(gè)拷貝構(gòu)造函數(shù):
X(X&& rhs);
有些人可能會(huì)認(rèn)為一個(gè)右值引用本身就是右值。但右值引用的設(shè)計(jì)者們采用了一個(gè)更微妙的標(biāo)準(zhǔn):
右值引用類型既可以被當(dāng)作左值也可以被當(dāng)作右值,判斷的標(biāo)準(zhǔn)是,如果它有名字,那就是左值,否則就是右值。
在上面的例子中,因?yàn)橛抑狄脁是有名字的,所以x被當(dāng)作左值來處理。
void foo(X&&void foo(X&& x){ X anotherX = x; // 調(diào)用X(X const & rhs)}
下面是一個(gè)沒有名字的右值引用被當(dāng)作右值處理的例子:
X&& goo();X x = goo(); // 調(diào)用X(X&& rhs),goo的返回值沒有名字
之所以采用這樣的判斷方法,是因?yàn)椋喝绻试S悄悄地把move語義應(yīng)用到有名字的東西(比如foo中的x)上面,代碼會(huì)變得容易出錯(cuò)和讓人迷惑。
void foo(X&& x){ X anotherX = x; // x仍然在作用域內(nèi)}
這里的x仍然是可以被后面的代碼所訪問到的,如果把x作為右值看待,那么經(jīng)過X anotherX = x;
后,x的內(nèi)容已經(jīng)發(fā)生變化。move語義的重點(diǎn)在于將其應(yīng)用于那些不重要的東西上面,那些move之后會(huì)馬上銷毀而不會(huì)被再次用到的東西上面。所以就有了上面的準(zhǔn)則:如果有名字,那么它就是左值。
那另外一半,“如果沒有名字,那它就是右值”又如何理解呢?上面goo()的例子中,理論上來說goo()所引用的對(duì)象也可能在X x = goo();
后被訪問的到。但是回想一下,這種行為不正是我們想要的嗎?我們也想隨心所欲的在左值上面使用move語義。正是“如果沒有名字,那它就是右值”的規(guī)則讓我們能夠?qū)崿F(xiàn)強(qiáng)制move語義。其實(shí)這就是std::move的原理。這里展示std::move的具體實(shí)現(xiàn)還是太早了點(diǎn),不過我們離理解std::move更近了一步。它什么都沒做,只是把它的參數(shù)通過右值引用的形式傳遞下去。
std::move(x)
的類型是右值引用,而且它也沒有名字,所以它是個(gè)右值。因此std::move(x)
正是通過隱藏名字的方式把它的參數(shù)變?yōu)橛抑怠?/p>
下面這個(gè)例子將展示記住“如果它有名字”的規(guī)則是多么重要。假設(shè)你寫了一個(gè)類Base,并且通過重載拷貝構(gòu)造函數(shù)和賦值操作符實(shí)現(xiàn)了move語義:
Base(Base const & rhs); // non-move semanticsBase(Base&& rhs); // move semantics
然后又寫了一個(gè)繼承自Base的類Derived。為了保證Derived對(duì)象中的Base部分能夠正確實(shí)現(xiàn)move語義,必須也重載Derived類的拷貝構(gòu)造函數(shù)和賦值操作符。先讓我們看下拷貝構(gòu)造函數(shù)(賦值操作符的實(shí)現(xiàn)類似),左值版本的拷貝構(gòu)造函數(shù)很直白:
Derived(Derived const & rhs) : Base(rhs){ // Derived-specific stuff}
但右值版本的重載卻要仔細(xì)研究下,下面是某個(gè)不知道“如果它有名字”規(guī)則的程序員寫的:
Derived(Derived&& rhs) : Base(rhs) // 錯(cuò)誤:rhs是個(gè)左值{ // ...}
如果像上面這樣寫,調(diào)用的永遠(yuǎn)是Base的非move語義的拷貝構(gòu)造函數(shù)。因?yàn)閞hs有名字,所以它是個(gè)左值。但我們想要調(diào)用的卻是move語義的拷貝構(gòu)造函數(shù),所以應(yīng)該這么寫:
Derived(Derived&& rhs) : Base(std::move(rhs)) // good, calls Base(Base&& rhs){ // Derived-specific stuff}
現(xiàn)在有這么一個(gè)函數(shù):
X foo(){ X x; // perhaps do something to x return x;}
一看到這個(gè)函數(shù),你可能會(huì)說,咦,這個(gè)函數(shù)里有一個(gè)復(fù)制的動(dòng)作,不如讓它使用move語義:
X foo(){ X x; // perhaps do something to x return std::move(x); // making it worse!}
很不幸的是,這樣不但沒有幫助反而會(huì)讓它變的更糟?,F(xiàn)在的編譯器基本上都會(huì)做返回值優(yōu)化(return value optimization)。也就是說,編譯器會(huì)在函數(shù)返回的地方直接創(chuàng)建對(duì)象,而不是在函數(shù)中創(chuàng)建后再復(fù)制出來。很明顯,這比move語義還要好一點(diǎn)。
所以,為了更好的使用右值引用和move語義,你得很好的理解現(xiàn)在編譯器的一些特殊效果,比如return value optimization和copy elision。并且在運(yùn)用右值引用和move語義時(shí)將其考慮在內(nèi)。Dave Abrahams就這一主題寫了一系列的文章[4]。
除了實(shí)現(xiàn)move語義之外,右值引用要解決的另一個(gè)問題就是完美轉(zhuǎn)發(fā)問題(perfect forwarding)。假設(shè)有下面這樣一個(gè)工廠函數(shù):
template<typename T, typename Arg>shared_ptr<T> factory(Arg arg){ return shared_ptr<T>(new T(arg));}
很明顯,這個(gè)函數(shù)的意圖是想把參數(shù)arg轉(zhuǎn)發(fā)給T的構(gòu)造函數(shù)。對(duì)參數(shù)arg而言,理想的情況是好像factory函數(shù)不存在一樣,直接調(diào)用構(gòu)造函數(shù),這就是所謂的“完美轉(zhuǎn)發(fā)”。但真實(shí)情況是這個(gè)函數(shù)是錯(cuò)誤的,因?yàn)樗肓祟~外的通過值的函數(shù)調(diào)用,這將不適用于那些以引用為參數(shù)的構(gòu)造函數(shù)。
最常見的解決方法,比如被boost::bind采用的,就是讓外面的函數(shù)以引用作為參數(shù)。
template<typename T, typename Arg>shared_ptr<T> factory(Arg& arg){ return shared_ptr<T>(new T(arg));}
這樣確實(shí)會(huì)好一點(diǎn),但不是完美的?,F(xiàn)在的問題是這個(gè)函數(shù)不能接受右值作為參數(shù):
factory<X>(hoo()); // error if hoo returns by valuefactory<X>(41); // error
這個(gè)問題可以通過一個(gè)接受const引用的重載解決:
template<typename T, typename Arg>shared_ptr<T> factory(Arg const & arg){ return shared_ptr<T>(new T(arg));}
這個(gè)辦法仍然有兩個(gè)問題。首先如果factory函數(shù)的參數(shù)不是一個(gè)而是多個(gè),那就需要針對(duì)每個(gè)參數(shù)都要寫const引用和non-const引用的重載。代碼會(huì)變的出奇的長(zhǎng)。
其次這種辦法也稱不上是完美轉(zhuǎn)發(fā),因?yàn)樗荒軐?shí)現(xiàn)move語義。factory內(nèi)的構(gòu)造函數(shù)的參數(shù)是個(gè)左值(因?yàn)樗忻郑约词箻?gòu)造函數(shù)本身已經(jīng)支持,factory也無法實(shí)現(xiàn)move語義。
右值引用可以很好的解決上面這些問題。它使得不通過重載而實(shí)現(xiàn)真正的完美轉(zhuǎn)發(fā)成為可能。為了弄清楚是如何實(shí)現(xiàn)的,我們還需要再掌握兩個(gè)右值引用的規(guī)則。
第一條右值引用的規(guī)則也會(huì)影響到左值引用?;叵胍幌拢赾++11標(biāo)準(zhǔn)之前,是不允許出現(xiàn)對(duì)某個(gè)引用的引用的:像A& &這樣的語句會(huì)導(dǎo)致編譯錯(cuò)誤。不同的是,在c++11標(biāo)準(zhǔn)里面引入了引用疊加規(guī)則:
A& & => A&A& && => A&A&& & => A&A&& && => A&&
另外一個(gè)是模版參數(shù)推導(dǎo)規(guī)則。這里的模版是接受一個(gè)右值引用作為模版參數(shù)的函數(shù)模版。
template<typename T>void foo(T&&);
針對(duì)這樣的模版有如下的規(guī)則:
有了上面這些規(guī)則,我們可以用右值引用來解決前面的完美轉(zhuǎn)發(fā)問題。下面是解決的辦法:
template<typename T, typename Arg>shared_ptr<T> factory(Arg&& arg){ return shared_ptr<T>(new T(std::forward<Arg>(arg)));}
std::forward的定義如下:
template<class S>S&& forward(typename remove_reference<S>::type& a) noexcept{ return static_cast<S&&>(a);}
上面的程序是如何解決完美轉(zhuǎn)發(fā)的問題的?我們需要討論當(dāng)factory的參數(shù)是左值或右值這兩種情況。假設(shè)A和X是兩種類型。先來看factory的參數(shù)是X類型的左值時(shí)的情況:
根據(jù)上面的規(guī)則可以推導(dǎo)得到,factory的模版參數(shù)Arg變成了X&,于是編譯器會(huì)像下面這樣將模版實(shí)例化:
應(yīng)用前面的引用疊加規(guī)則并且求得remove_reference的值后,上面的代碼又變成了這樣:
這對(duì)于左值來說當(dāng)然是完美轉(zhuǎn)發(fā):通過兩次中轉(zhuǎn),參數(shù)arg被傳遞給了A的構(gòu)造函數(shù),這兩次中轉(zhuǎn)都是通過左值引用完成的。
現(xiàn)在再考慮參數(shù)是右值的情況:
再次根據(jù)上面的規(guī)則推導(dǎo)得到:
對(duì)右值來說,這也是完美轉(zhuǎn)發(fā):參數(shù)通過兩次中轉(zhuǎn)被傳遞給A的構(gòu)造函數(shù)。另外對(duì)A的構(gòu)造函數(shù)來說,它的參數(shù)是個(gè)被聲明為右值引用類型的表達(dá)式,并且它還沒有名字。那么根據(jù)第5節(jié)中的規(guī)則可以判斷,它就是個(gè)右值。這意味著這樣的轉(zhuǎn)發(fā)完好的保留了move語義,就像factory函數(shù)并不存在一樣。
事實(shí)上std::forward的真正目的在于保留move語義。如果沒有std::forward,一切都是正常的,但有一點(diǎn)除外:A的構(gòu)造函數(shù)的參數(shù)是有名字的,那這個(gè)參數(shù)就只能是個(gè)左值。
如果你想再深入挖掘一點(diǎn)的話,不妨問下自己這個(gè)問題:為什么需要remove_reference?答案是其實(shí)根本不需要。如果把remove_reference<S>::type&
換成S&
,一樣可以得出和上面相同的結(jié)論。但是這一切的前提是我們指定Arg作為std::forward的模版參數(shù)。remove_reference存在的原因就是強(qiáng)迫我們?nèi)ミ@樣做。
已經(jīng)講的差不多了,剩下的就是std::move的實(shí)現(xiàn)了。記住,std::move的用意在于將它的參數(shù)傳遞下去,將它轉(zhuǎn)換成右值。
下面假設(shè)我們針對(duì)一個(gè)X類型的左值調(diào)用std::move。
根據(jù)前面的模版參數(shù)推導(dǎo)規(guī)則,模版參數(shù)T變成了X&,于是:
然后求得remove_reference的值,并應(yīng)用引用疊加規(guī)則,得到:
這就可以了,x變成了沒有名字的右值引用。
參數(shù)是右值的情況由你來自己推導(dǎo)。不過你可能馬上就想跳過去了,為什么會(huì)有人把std::move用在右值上呢?它的功能不就是把參數(shù)變成右值么。另外你可能也注意到了,我們完全可以用static_cast<X&&>(x)
來代替std::move(x)
,不過大多數(shù)情況下還是用std::move(x)
比較好。
本文轉(zhuǎn)自:[譯]詳解C++右值引用
聯(lián)系客服