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