移动语义-右值引用-完美转发-万字长文让你一探究竟
C++ 右值引用
block://6984617523950616580?from=docs_block&id=ce31003ceb5efb1f7a7c0a5fbe6cb60191627a38
右值的引入
作為在C++11中引入的一個類型,容易引起誤解的是,右值引用并沒有說明引入是為了什么,是為了解決什么問題。
右值引用可以解決以下問題
左值和右值來自原先的C語言,左值可以出現在賦值左邊或者右邊,而右值只能出現在賦值的右邊
int a = 42; int b = 43;// a and b are both l-values: a = b; // ok b = a; // ok a = a * b; // ok// a * b is an rvalue: int c = a * b; // ok, rvalue on right hand side of assignment a * b = 42; // error, rvalue on left hand side of assignment在 C++ 中,這作為第一個直觀的左值和右值方法仍然很有用。但是,帶有用戶定義類型的 C++ 引入了一些關于可修改性和可分配性的微妙之處,導致此定義不正確。我們沒有必要進一步討論這個問題。這是一個替代定義,盡管它仍然存在爭議,但它將使您能夠處理右值引用:左值是一個引用內存位置的表達式,并允許我們通過&操作符取得地址,右值,不是左值的都是右值。
// lvalues: // int i = 42; i = 43; // ok, i is an lvalue int* p = &i; // ok, i is an lvalue int& foo(); foo() = 42; // ok, foo() is an lvalue int* p1 = &foo(); // ok, foo() is an lvalue// rvalues: // int foobar(); int j = 0; j = foobar(); // ok, foobar() is an rvalue int* p2 = &foobar(); // error, cannot take the address of an rvalue j = 42; // ok, 42 is an rvalue移動語義
假設有一個類X,類中的成員變量m_pResource是一個需要花費時間和內存取進行構造和析構的類型,比如m_pResource是一個vector類型,對其進行賦值時將會產生大量的析構和構造函數的調用。
X& X::operator=(X const & rhs) {// [...]// Make a clone of what rhs.m_pResource refers to.// Destruct the resource that m_pResource refers to. // Attach the clone to m_pResource.// [...] }同樣的問題會出現在copy構造函數上
X foo(); X x; // perhaps use x in various ways x = foo();-
clones the resource from the temporary returned by foo,
-
destructs the resource held by x and replaces it with the clone,
-
destructs the temporary and thereby releases its resource.
當賦值操作符的右邊是右值的話,只是交換值的指針是比較高效的
// [...] // swap m_pResource and rhs.m_pResource // [...]上述這種操作就是移動語義,可以通過操作符重載實現
X& X::operator=(<mystery type> rhs) {// [...]// swap this->m_pResource and rhs.m_pResource// [...] }以上調用無論是賦值還是copy構造函數,都會導致大量的構造函數和析構函數調用(如當vector中存儲很多的類對象時),因此我們當然希望能夠實現對傳入類型的引用,從而避免這些構造函數和析構函數的調用
block://6984620384730546178?from=docs_block&id=ce31003ceb5efb1f7a7c0a5fbe6cb60191627a38
右值引用
如果X是一個類型,那么X&& 就是對X類型的右值引用,為了更好的區分X&被稱為左值引用
一個右值引用類型很多地方表現與左值引用相同,除了一些例外。最重要的一條就是,當進行函數重載的時候,左值當成參數傳入函數,偏向調用左值引用的函數;當右值傳入函數時,更加偏向調用右值重載的函數
void foo(X& x); // 左值函數重載 void foo(X&& x); // 右值函數重載X x; X foobar();foo(x); // argument is lvalue: calls foo(X&) foo(foobar()); // argument is rvalue: calls foo(X&&)Rvalue references allow a function to branch at compile time (via overload resolution) on the condition “Am I being called on an lvalue or an rvalue?”
大體意思就是,右值引用允許編譯器期間通過是右值還是左值調用不同的函數
當然你可以使用上述方法重載任何函數,就像上述所示。但是通常會被用于重載拷貝構造函數和賦值構造函數,用來實現移動語義
X& X::operator=(X const & rhs); // classical implementationX& X::operator=(X&& rhs) {// Move semantics: exchange content between this and rhsreturn *this; } Note: If you implement void foo(X&); but not void foo(X&&); then of course the behavior is unchanged: foo can be called on l-values, but not on r-values. If you implement void foo(X const &); but not void foo(X&&); then again, the behavior is unchanged: foo can be called on l-values and r-values, but it is not possible to make it distinguish between l-values and r-values. That is possible only by implementing void foo(X&&); as well. Finally, if you implement void foo(X&&); but neither one of void foo(X&); and void foo(X const &); then, according to the final version of C++11, foo can be called on r-values, but trying to call it on an l-value will trigger a compile error.強制移動語義
我們都知道,在給予更多控制權和避免粗心大意犯錯方面C++選擇給予更多的控制權,你不但可以在右值上實現移動語義,而且你可以自行決定在左值上實現移動語義,一個很好的例子就是std::swap函數
template<class T> void swap(T& a, T& b) { T tmp(a);a = b; b = tmp; } X a, b; swap(a, b);這里沒有使用右值,因此有沒有實現移動語義,但是我們知道實現移動語義會更好,只要變量作為復制構造或者賦值的源出現,該變量要么根本就不再使用,要么就作為賦值的目標。
C++11中與一個被調用的庫函數std::move可以將其參數轉換 右值, 不做其他事情
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);修改之后上述三行實現了移動語義,需要注意的是,對于那些沒有實現移動語義的類型(即:沒有使用右值引用版本重載它們的復制構造函數和賦值運算符),對于這些類型新的swap就和舊的一樣
既然、知道了移動語義std::move,如下:
a = b;你期望在這里發生什么?你期望a持有的對象被b的復制出來的副本替換,并且希望a先前持有的對象析構,現在我們考慮一下語義:
a = std::move(b);如果實現了移動語義,會交換a和b持有的對象,不會有任何對象進行析構。當然結束之后a原先持有的對象的生命周期將和b的作用范圍綁定,b超出范圍a原先持有的對象將會被銷毀。
所以從某種意義上說,我們在這里陷入了非確定性破壞的陰暗世界:一個變量已被分配,但該變量以前持有的對象仍在某處。只要該對象的銷毀不會產生任何外界可見的副作用,就可以了。但有時析構函數確實有這樣的副作用。一個例子是釋放析構函數內的鎖。因此,具有副作用的對象銷毀的任何部分都應該在復制賦值運算符的右值引用重載中顯式執行:
X& X::operator=(X&& rhs) {// Perform a cleanup that takes care of at least those parts of the// destructor that have side effects. Be sure to leave the object// in a destructible and assignable state.// Move semantics: exchange content between this and rhsreturn *this; }右值引用就是右值嗎?
像以前一樣,我們為X實現復制構造函數和賦值操作符重載來實現移動語義。
假如:
void foo(X&& x) {// x是右值引用,但是x本身是一個左值,以為x是有命名的X anotherX = x; // 調用右值引用賦值重載函數還是左值???// ... }代碼中函數內x是一個左值引用,然而我們期望讓右值引用就是本身就是右值。右值引用的設計者提供了一個更好的思路:
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.
大意就是,右值引用可以是左值也可以是右值,評判的標準是,如果這個值有命名就是左值,如果沒有就是右值。
那么上述代碼中,雖然參數傳進的是右值,但是進入函數的時候,因為x已經有命名了,所以函數內部的x是左值,那么函數內部調用的也是左值的賦值函數
void foo(X&& x) {X anotherX = x; // calls X(X const & rhs) }如下是一個沒有名字的右值,因此會調用右值賦值函數
X&& goo(); X x = goo(); // calls X(X&& rhs) because the thing on// the right hand side has no name這種設計的背后思路就是:允許移動語義應用于一些有名字的對象
X anotherX = x;// x is still in scope!以上語句是非常危險的,移動的食物應該在移動后立即死亡并消失,因此有一條規則,如果它有一個名字,那么它就是左值
如果沒有名字,那么他就是個右值,如果有名字需要使用std::move()進行轉換,std::move()通過將其參數轉換為右值,即使這個這個參數不是右值。
在編程的過程中,時刻注意變量是否有一個名字,也就是注意變量是否是右值非常的重要。
假設你實現了一個基類,當然為了實現移動語義你要給基類實現復制構造函數和復制操作符重載
Base(Base const & rhs); // non-move semantics Base(Base&& rhs); // move semantics現在假設已實現了一個類Derived繼承了Base基類,為了確保Derived類中繼承的Base也實現了移動語義,你必須實現Derived的復制構造函數和賦值操作符,我們先看下復制構造函數的重載
Derived(Derived const & rhs) : Base(rhs) {// Derived-specific stuff }可以看到很簡潔,只需要將傳遞個Derived的參數,復制構造的時候傳遞給Base就可以了,那我們來看下移動復制構造函數的實現:
Derived(Derived&& rhs) : Base(rhs) // wrong: rhs is an lvalue {// Derived-specific stuff }如果我們這樣調用,那么將會調用基類的非移動語義的復制構造函數,因為rhs傳遞給Base的時候,是有名字的,所以是按照左值傳遞的。如果我們想按照移動語義進行調用,我們可以按照如下的方式實現:
Derived(Derived&& rhs) : Base(std::move(rhs)) // good, calls Base(Base&& rhs) {// Derived-specific stuff }移動語義和編譯優化
考慮到有如下函數定義:
X foo() {X x;// perhaps do something to xreturn x; }現在想象一下,我們通過對X類重載復制構造函數和賦值操作符實現了移動語義,如果你只從表面上來看,上述代碼中x變量,在進行return的時候,會存在值得復制,就是局部變量x復制給返回值,來讓我們使用移動語義優化一下吧:
X foo() {X x;// perhaps do something to xreturn std::move(x); // making it worse! }實際上這樣寫之后,會使事情變得比以前更糟。因為現代的編譯器都會實行返回值優化(RVO),換句話說,比起構造一個局部的x對象,并將其復制出去,編譯器更傾向于直接構造返回值并將其按照引用的方式在函數內部使用。
例如:
class X {public:X() {cout << "Construct X " << endl;}~X() {cout << "Destruct X" << endl;} };X ReturnValueOptimization() {X x;return x; }int main(int argc, char* argv[]) {X retValue = ReturnValueOptimization();return 0; }函數執行返回
$ ./return_value_optimization Construct X Destruct X如果沒有執行返回值優化,那么按照代碼字面的意思,正常的應該是要進行兩次構造和析構,但是從執行結果可以看出,實際上只執行了一次構造和析構函數,因此在有返回值的情況下,現在的編譯器都會對其進行返回值優化
所以為了正確的使用右值和移動語義,你需要充分考慮當今編譯器的特殊效果。例如返回值優化和復制省略等;
完美轉發的問題
另外一個需要基于右值引用實現的移動語義來進行解決的問題就是完美轉發,看下如下函數:
template<typename T, typename Arg> shared_ptr<T> factory(Arg arg) { return shared_ptr<T>(new T(arg)); }顯然函數的目的是為了實現從factory函數將參數完美轉發給T的構造函數,理想的情況下,應該能夠實現就像外層的factory函數不存在一樣,并且構造函數能夠直接調用用戶傳進來的參數–這就是完美轉發。上面函數的問題就是factory函數按照值進行參數傳遞,更壞的情況是如果T的構造函數按照引用用參數,那么將帶來嚴重的后果。
好一點的改進就是,factory函數按照引用來進行參數傳遞,如boost::bind函數一樣
template<typename T, typename Arg> shared_ptr<T> factory(Arg& arg) { return shared_ptr<T>(new T(arg)); }改進之后相對于按照值傳遞好一點,但是并不是很完美,問題在于factory函數不能按照右值調用。
factory<X>(hoo()); // error if hoo returns by value factory<X>(41); // error當然,這樣的問題能夠使用增加const修飾符來解決:
template<typename T, typename Arg> shared_ptr<T> factory(Arg const & arg) { return shared_ptr<T>(new T(arg)); }當然使用const還是會存在問題,首先如果factory不止有一個參數,你必須為所有參數的組合提供函數的const重載,因此對于多個參數的解決方案非常的有限。
其次,這種轉發并不夠完美,因為factory函數內的arg是一個左值,因此移動語義不可能發生,即使沒有factory函數題也不會發生
最后,我們可以使用移動語義解決上述的兩個問題,移動語義可以實現真實意義上的完美轉發,為了了解如何實現,我們需要知道另外兩個右值引用的規則:
完美轉發的解決方式
第一條:右值引用的規則,同樣也會影響左值引用。C++11之前不允許使用引用的引用,如果A& &將會造成編譯錯誤,C++11中中引入了一下折疊規則:
- A& & --> A&
- A& && --> A&
- A&& & --> A&
- A&& && --> A&
第二條: 對于使用右值引用的模板函數,有一個特殊的模板推倒規則
template<typename T> void foo(T&&);因此定義一個模板函數,能夠實現對左值和右值的同時支持。
有了以上的規則,我們就可以著手解決上述遇到的不能完美轉發的問題:
template<typename T, typename Arg> shared_ptr<T> factory(Arg&& arg) { return shared_ptr<T>(new T(std::forward<Arg>(arg))); } where std::forward is defined as follows:template<class S>S&& forward(typename remove_reference<S>::type& a) noexcept {return static_cast<S&&>(a); }不用關注noexcept,它只是為了告訴編譯器編譯優化意圖,該函數不會拋出任何異常。
假設:factory函數被一個類型為X的左值調用:
X x; factory<A>(x);然后根據折疊規則,factory的模板參數Arg將會被解析成X&,編譯器將會生成如下的factory函數和std::forward
shared_ptr<A> factory(X& && arg) { return shared_ptr<A>(new A(std::forward<X&>(arg))); } X& && forward(remove_reference<X&>::type& a) noexcept {return static_cast<X& &&>(a); }經過折疊之后
shared_ptr<A> factory(X& arg) { return shared_ptr<A>(new A(std::forward<X&>(arg))); } X& std::forward(X& a) {return static_cast<X&>(a); }這樣左值也實現了完美轉發,工廠函數經過兩次間接傳遞,將參數arg傳遞給構造函數,并且是通過老式的左值引用。
現在我們假設工廠函數factory被一個類型為X的右值調用:
X foo(); factory<A>(foo());經過折疊規則之后factory函數將如下:
shared_ptr<A> factory(X&& arg) { return shared_ptr<A>(new A(std::forward<X>(arg))); } X&& forward(X& a) noexcept {return static_cast<X&&>(a); }該模板函數實現了對右值的完美轉發,經過兩次引用傳遞之后A的構造函數還是拿到了右值的arg參數,并且經過forward轉發之后,A構造函數拿到的變量沒有名字。因此根據無"無名"規則,該變量就是一個右值。因此將調用A的右值構造函數
接下來讓我們看下std::move(),該函數知識實現了將傳給其的參數轉綁定成像右值一樣,如下是其實現:
template<class T> typename remove_reference<T>::type&& std::move(T&& a) noexcept {typedef typename remove_reference<T>::type&& RvalRef;return static_cast<RvalRef>(a); }假設我們向std::move()傳遞一個類型為X的左值:
X x; std::move(x);根據新的模板推導付規則,模板參數T會被解析成X&,因此編譯器最終實例化是:
typename remove_reference<X&>::type&& std::move(X& && a) noexcept {typedef typename remove_reference<X&>::type&& RvalRef;return static_cast<RvalRef>(a); }經過參數折疊和remove_reference的作用之后,會生成如下代碼:
X&& std::move(X& a) noexcept {return static_cast<X&&>(a); }到這里std::move()所實現的事情就一目了然了,就是接收左值引用,并將其轉換為無明明的右值引用。
右值引用和異常
當你采用C++進行代碼開發時,是否使用在你的代碼里使用異常處理是你決定的事情,但是右值引用比較特殊。當你為一個類實現移動語義,進行復制構造函數重載和賦值函數實現的時候,必須要遵循一下規則:
如果你沒有實現上述兩種規則,那么至少一種常見的場景下是不能使用你定義的移動語義的:當一個std::vector()被調整大小的時候,你希望調整大小的時候能發生移動語義,但是除非你實現以上兩條規則否則移動語義在這里不會發生-- Effective Modern C++條
隱式移動
在右值引用的復雜討論中,標準委員會提出移動構造函數、移動賦值操作符,編譯器應當自動生成-在用戶沒有提供的情況下。這看起來很正常,因為編譯器會在用戶沒有提供的情況下,自動的提供構造函數和賦值操作符的默認實現。但是Scott Meyers向編譯器提交了一個消息posted a message on comp.lang.c++,里面詳細論述了如果編譯器提供移動語義的構造函數和賦值操作符的實現,將會對以前已經存在的代碼引入一個非常嚴重的問題。當然也可以參考Scott Meyers的Effective Modern C++的地17條
推薦:
C++右值引用
Implicit Move Must Go
A Brief Introduction to Rvalue References
Implicit Move Must Go
C++ Rvalue references Explained
總結
以上是生活随笔為你收集整理的移动语义-右值引用-完美转发-万字长文让你一探究竟的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: RabbitMq队列 queue
- 下一篇: RabbitMq 发布订阅 Publis