這篇文章主要談談c++11中引入的右值引用概念和移動語義概念。以及這些東西可能在我們編程中帶來哪些體驗、便捷或者是代碼效率的提高。 文章主要分為以下三點:
臨時對象的產生何謂右值引用何謂移動語義在我們以往的編程過程中可能很少會注意到臨時對象(變量)的問題,因為這已經不太是程序層面上的問題,而更多是編譯器上的事情。編譯器在編譯代碼過程中為了實現某些代碼可能會產生出一些臨時對象(變量)來滿足一些效果,比如:
void Test(MyClass obj) {}MyClass myclass;Test(myclass);//可能編譯器會做如下改寫:void Test(MyClass& obj){}MyClass myclass;MyClass obj(myclass); //產生的臨時對象Test(obj);obj.~MyClass();又或者是這樣:
double db = 5.5;int it = st; //產生int臨時變量賦值給it當然,這些臨時對象并不需要程序員參與干涉,甚至臨時對象這些行為對于程序員而言是透明的。但是,我們仍然需要對其進行一定的了解,這對后面c++11引入的右值引用和移動語義的理解會帶來莫大的幫助。
產生臨時變量的地方有很多,不同編譯器在某些細節上可能處理方式也略有不同。不過具體說來有以下幾種:
不同類型(對象)變量的轉換函數以pass by value傳遞參數的時候表達式求值中這里文章就不繼續深入探討了,下面我們來談臨時對象對編程的一些影響和右值引用的引入。
假如我們設計一個復數類,可能大概如下:
class Complex{public: Complex() :_real(0), _imaginary(0) {} Complex(double real, double imaginary) :_real(real), _imaginary(imaginary) {} Complex(Complex& cp) :_real(cp._real), _imaginary(cp._imaginary) {}PRivate: double _real; //實部 double _imaginary; //虛部};嗯,先不看其他函數,就此而言大概是”沒有太多問題的”。 但是如果在如下使用時候:
//求共軛復數//為Complex的友元Complex getConjugate(Complex cp){ cp._imaginary = -cp._imaginary; return cp;}int main(){ Complex a(1.0, -5.0); Complex b = getConjugate(a); // Error : no match return 0;}有的編譯器下會報錯。MSVC 沒有, GUN GCC 報錯了。原因呢很簡單,編譯器提示找不到匹配的拷貝構造函數。 我們在前面的介紹知道,這里函數返回的其實是一個臨時對象,而我們拷貝構造函數中的參數類型為Complex&。而這里,試圖將一個引用綁定到臨時變量上,這顯然是不允許的。因為,規定不允許改變臨時變量。 所以我們也知道了解決方案:將拷貝構造函數修改為 Complex(const Complex& cp)
以上問題就告一段落了,我們來到一個新問題,我們試圖寫一個mystring類:
class MyString{ friend ostream& Operator << (ostream& os, MyString mystr);public: MyString() :_ptr(0) {} MyString(const char* ptr) { int len = strlen(ptr); _ptr = new char[len + 1]; strcpy(_ptr, ptr); } MyString(const MyString& mystr) { int len = strlen(mystr._ptr); _ptr = new char[len + 1]; strcpy(_ptr, mystr._ptr); } //MyString& operator = (const MyString&){ ... } ~MyString() { delete _ptr; }private: char* _ptr;};ostream& operator << (ostream& os, MyString mystr){ return os << mystr._ptr;}int main(){ MyString a = "123"; cout << a << endl; return 0;}由于篇幅,我就只是給出了必要的函數,operator=()就沒有給出了。所以賦值只是淺拷貝,不過這里并不關注這個。
我們注意看拷貝構造函數,由于前篇提到的原因,這里的拷貝函數我們加上了const,使得拷貝函數能夠接受臨時變量。 當傳入參數是臨時變量的情況時,我們仔細思考一下: 我們知道臨時變量很快(大概就是函數返回之后)就會銷毀,然而在這樣的情況下,行為仿佛是:將一份字符串拷貝一份,然后就將原稿銷毀了。對,為什么不能將臨時對象的字符串”拿為己用”呢?這樣我們就免去了內存的開辟和繁瑣的字符串賦值了。嗯,大概函數如下:
MyString(....):_ptr(mystr._ptr){ mystr._ptr = NULL;}對,這樣我們就將_ptr指向的真正的字符串的所有權拿過來了。但是,這里我把參數空了出來,是的,我們要去設法判斷什么時候是臨時對象。然而這在c++1.0是不太可行的。
所以,在C++11中引入了一個新的概念——右值引用(&&),右值引用專門用于引用右值(臨時對象、匿名對象)。
那么有了語言的支持,對于上面的情況,我們可以進行如下的改寫:
class MyString{ friend ostream& operator << (ostream& os, MyString mystr);public: //上面不變... MyString(const MyString& mystr) { int len = strlen(mystr._ptr); _ptr = new char[len + 1]; strcpy(_ptr, mystr._ptr); } MyString(MyString&& mystr) :_ptr(mystr._ptr){ mystr._ptr = NULL; } //下面不變...};這樣的話,我們當傳入臨時變量的時候,哦,不對,我們應該改口叫做右值。 那么調用的編譯器就是MyString(MyString&& mystr)版本,在函數中,我們剝奪了右值的字符串,并且將mystr._ptr = NULL, 這很重要,因為防止臨時變量析構的時候銷毀字符串。
其實上一段在介紹mystring設計的時候已經使用了移動語義,就是程序員提供移動構造函數(參數為右值引用的構造函數)使得當用臨時變量構造新對象的時候可以提供較好的優化。 下面給就用上面的mystring作為例子來測一下提供了移動構造函數和沒有提供移動構造函數的時候,分別用臨時變量去構造對象的對比:
| 100000長度字符串,10000次初始化 | |
|---|---|
| 提供移動構造 | 22ms |
| 不提供移動構造 | 376ms |
可以看出來提供移動構造函數在某些情況下可以將性能極大地提高。特別對于某些需要深拷貝的對象來說。特別的,當這一類的對象配合標準庫的容器的時候(vector,deque),如果提供移動構造函數,將會在容器內存重分配的時候帶來極大效率的優化。
當然,除了編譯器創建的臨時對象來作為右值以外,我們也可以使用std::move來得到一個左值的右值引用。 我們先來看move()的源碼:
template<class _Ty> inlineconstexpr typename remove_reference<_Ty>::type&&move(_Ty&& _Arg) _NOEXCEPT{ return (static_cast<typename remove_reference<_Ty>::type&&>(_Arg));}當傳入左值Type&的時候 _Ty被識別為Type& , remove_reference<_Ty>::type 為 Type,返回的就是 Type&& 當傳入右值Type&&的時候_Ty被識別為Type&&, remove_reference<_Ty>::type 為 Type,返回的就是 Type&&
也就是無論傳入一個什么值,都將返回這個值的右值引用,這樣就可以顯式讓編譯器調用類中的移動構造函數了。不過值得注意的是,程序員必須恪守——被當作右值的對象在移動構造之后在重新賦值之前絕不使用。 如下:
vector<int> a = {1,2,3,4,5};vector<int> b(move(a)); //這里調用的是b的移動構造函數//a在重新賦值之前絕不使用。新聞熱點
疑難解答
圖片精選