【從零學C++11(中)】移動語義、右值引用、std::move()、完美轉發等新特性


8. 默認函數控制

C++中對於空類編譯器會生成一些默認的成員函數,比如:構造函數、拷貝構造函數、運算符重載、析構函數、&和const&的重載、移動構造、移動拷貝構造等函數。

如果在類中顯式定義了,編譯器將不會重新生成默認版本。有時候這樣的規則可能被忘記,最常見的是聲明瞭帶參數的構造函數,必要時則需要定義不帶參數的版本以實例化無參的對象。而且有時編譯器會生成,有時又不生成,容易造成混亂,於是C++11讓程序員可以控制是否需要編譯器生成

顯式缺省函數

C++11中,可以在默認函數定義或者聲明時加上=default,從而顯式的指示編譯器生成該函數的默認版本,用=default修飾的函數稱爲顯式缺省函數

class A{
public:
	A(int a)
	 : _a(a)
	{}
	
	// 顯式缺省構造函數,由編譯器生成
	A() = default;
	
	// 可以選擇在類中聲明,在類外定義時讓編譯器生成默認賦值運算符重載
	A& operator=(const A& a);
private:
	int _a;
};

A& A::operator=(const A& a) = default;		//類外定義

int main(){
	A a1(10);
	A a2;
	a2 = a1;
	return 0;
}

刪除默認函數

如果能想要限制某些默認函數的生成:

  • C++98中,是該函數設置成private,並且不完成實現,這樣只要其他人想要調用就會報錯。
  • C++11中更簡單,只需在該函數聲明加上=delete即可,該語法指示編譯器不生成對應函數的默認版本,稱=delete修飾的函數爲刪除函數
class A{
public:
	A(int a)
	 : _a(a)
	{}
	
	 // 禁止編譯器生成默認的拷貝構造函數以及賦值運算符重載
	A(const A&) = delete;
	A& operator(const A&) = delete;
private:
	int _a;
};

int main(){
	A a1(10);
	A a2(a1);
	// 編譯失敗,因爲該類沒有拷貝構造函數
	
	A a3(10);	
	a3 = a2;	
	// 編譯失敗,因爲該類沒有賦值運算符重載
	
	return 0;
}

9. 右值引用【★】

移動語義

如果一個類中涉及到資源管理,用戶必須顯式提供拷貝構造、賦值運算符重載以及析構函數,否則編譯器將會自動生成一個默認的,如果遇到拷貝對象或者對象之間相互賦值,就會出錯,比如:

class String{
public:
	String(char* str = ""){
		if (nullptr == str)
			str = "";
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}

	String(const String& s)
		: _str(new char[strlen(s._str) + 1])
	{
		strcpy(_str, s._str);
	}

	String& operator=(const String& s){
		if (this != &s){
			char* pTemp = new char[strlen(s._str) + 1];
			strcpy(pTemp, s._str);
			delete[] _str;
			_str = pTemp;
		}
		return *this;
	}

	~String(){
		if (_str) delete[] _str;
	}
private:
	char* _str;
};

假設現在有一個函數,返回值爲一個String類型的對象:

String GetString(char* pStr){
	String strTemp(pStr);
	return strTemp;		//此時不是返回棧上對象strTemp,而是拷貝構造一個臨時對象返回
}
int main(){
	String s2(GetString("world"));	
	/* 用GetString返回的臨時對象構造s2
		s2構造完成後臨時對象將被銷燬,因爲臨時對象臨時對象不能直接返回
		因此編譯器需要拷貝構造一份臨時對象,然後將strTemp銷燬
	*/
	return 0;
}

上述代碼看起來沒有什麼問題,但是有一個不太盡人意的地方:GetString函數返回的臨時對象,將s2拷貝構造成功之後,立馬被銷燬了(臨時對象的空間被釋放),再沒有其他作用;
s2在拷貝構造時,又需要分配空間,一個剛釋放一個又申請,有點多此一舉。
那能否將GetString返回的臨時對象的空間直接交給s2呢?這樣s2也不需要重新開闢空間了,代碼的效率會明顯提高。

  • 將一個對象中資源移動到另一個對象中的方式,稱之爲移動語義
  • C++11中如果需要實現移動語義,必須使用右值引用
String(String&& s)		//兩個 &
	: _str(s._str)
	{
		 s._str = nullptr; 
	} 

C++11中的右值

右值引用,顧名思義就是對右值的引用。C++11中,右值由兩個概念組成:純右值將亡值

  • 純右值
    純右值是C++98中右值的概念,用於識別臨時變量一些不跟對象關聯的值
    比如:常量一些運算表達式(1+3)等。
  • 將亡值
    聲明週期將要結束的對象。比如:在值返回時的臨時對象

右值引用

右值引用書寫格式:

類型&& 引用變量名字 = 實體;

右值引用最長常見的一個使用地方就是:與移動語義結合,減少無必要資源的開闢來提高代碼的運行效率

改造一下剛纔的例子代碼演示

String&& GetString(char* pStr){
	String strTemp(pStr);
	return strTemp;
}

int main(){
	String s1("hello");
	String s2(GetString("world"));
	return 0;
}

右值引用另一個比較常見的地方是:給一個匿名對象取別名,延長匿名對象的聲明週期。

String GetString(char* pStr){
	return String(pStr);
}
int main(){
	String&& s = GetString("hello");
	return 0;
}

【注】:

  1. 與引用一樣,右值引用在定義時必須初始化
  2. 通常情況下,右值引用不能引用左值
int main(){
	int a = 10;
	int&& ra; // 編譯失敗,沒有進行初始化
	int&& ra = a; // 編譯失敗,a是一個左值
	
	const int&& ra = 10;	// ra是匿名常量10的別名
	return 0;
}

std::move()

C++11中,std::move()函數位於<utility> 頭文件中,這個函數名字具有迷惑性,它並不搬移任何東西,唯一的功能就是將一個左值強制轉化爲右值引用,通過右值引用使用該值,實現移動語義。

注意:被轉化的左值,其生命週期並沒有隨着左右值的轉化而改變,即std::move轉化的左值變量left_value不會被銷燬。

  • 下面舉一個move()誤用的例子:
// 移動構造函數
class String{
	String(String&& s) 
	 : _str(s._str){
		s._str = nullptr;
	}
};
int main(){
	String s1("hello world");
	String s2(move(s1));
	String s3(s2);
	return 0;
}

move()更多的是用在生命週期即將結束的對象上。

【注】:爲了保證移動語義的傳遞,程序員在編寫移動構造函數時,最好使std::move轉移擁有資源的成員爲右值。

注意點

  1. 如果將移動構造函數聲明爲常右值引用或者返回右值的函數聲明爲常量,都會導致移動語義無法實現。
String(const String&&);
const Person GetTempPerson();
  1. C++11中,無參構造函數 / 拷貝構造函數 / 移動構造函數實際上有3個版本:
Object();
Object(const T&);
Object(T &&);
  1. C++11中默認成員函數
    默認情況下,編譯器會爲程序員隱式生成一個(如果沒有用到則不會生成)移動構造函數。如果程序員聲明瞭自定義的構造函數、移動構造、拷貝構造函數、賦值運算符重載、移動賦值、析構函數,編譯器都不會再爲程序員生成默認版本。編譯器生成的默認移動構造函數實際和默認的拷貝構造函數類似,都是按照位拷貝(即淺拷貝)來進行的。因此,在類中涉及到資源管理時,程序員最好自己定義移動構造函數。其他類有無移動構造都無關緊要。但在C++11中,拷貝構造/移動構造/賦值/移動賦值函數必須同時提供,或者同時不提供,程序才能保證類同時具有拷貝和移動語義。

完美轉發

完美轉發是指:在函數模板中,完全依照模板的參數的類型,將參數傳遞給函數模板中調用的另外一個函數。

void Func(int x){
	// ......
}
template<typename T>
void PerfectForward(T t){
	Fun(t);
}

PerfectForward爲轉發的模板函數,Func爲實際目標函數,但是上述轉發還不算完美:

  • 完美轉發是:目標函數總希望將參數按照<傳遞給轉發函數的實際類型>轉給目標函數,而不產生額外的開銷,就好像轉發者不存在一樣。

  • 所謂完美:函數模板在向其他函數傳遞自身形參時,如果相應實參是左值,它就應該被轉發爲左值;如果相應實參是右值,它就應該被轉發爲右值。這樣做是爲了保留在其他函數針對轉發而來的參數的左右值屬性進行不同處理(比如參數爲左值時實施拷貝語義;參數爲右值時實施移動語義)。

C++11通過forward函數來實現完美轉發, 比如:

void Fun(int &x) { cout << "lvalue ref" << endl; }
void Fun(int &&x) { cout << "rvalue ref" << endl; }
void Fun(const int &x) { cout << "const lvalue ref" << endl; }
void Fun(const int &&x) { cout << "const rvalue ref" << endl; }

template<typename T>
void PerfectForward(T &&t) { Fun(std::forward<T>(t)); }

int main(){
	PerfectForward(10); // rvalue ref
	
	int a;
	PerfectForward(a); // lvalue ref
	PerfectForward(std::move(a)); // rvalue ref
	const int b = 8;
	PerfectForward(b); // const lvalue ref
	PerfectForward(std::move(b)); // const rvalue ref
	return 0;
}

感謝您閱讀至此,感興趣的看官們可以移步上篇與下篇,繼續瞭解C++11剩餘新特性~

【從零學C++11(上)】列表初始化decltype關鍵字、委派構造等新特性
https://blog.csdn.net/qq_42351880/article/details/100140163

【從零學C++11(下)】lambda表達式、線程庫原子操作庫等新特性
https://blog.csdn.net/qq_42351880/article/details/100144882

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章