C++11(列表初始化+變數型別推導+型別轉換+左右值概念、引用+完美轉發和萬能應用+定位new+可變引數模板+emplace介面)

一隻少年AAA發表於2022-12-28

列表初始化

用法

在C++98中,{}只能夠對陣列元素進行統一的列表初始化,但是對應自定義型別,無法使用{}進行初始化,如下所示:

// 陣列型別
int arr1[] = { 1,2,3,4 };
int arr2[6]{ 1,2,3,4,5,6 };
// 自定義型別(C++98不支援下面這種初始化的方式)
vector<int> v{ 1,2,3 };

在C++11中,擴大了用大括號括起的列表(初始化列表)的使用範圍,使其可用於所有的內建型別使用者自定義的型別,使用初始化列表時,可新增等號(=),也可不新增,如下所示:

// 內建型別變數
int a{ 2 };
int b = { 3 };
int c = { a + b };

// 動態陣列 C++98不支援
int* arr = new int[5]{ 1,2,3,4,5 };
// 容器使用{}進行初始化
// vector<int> v = { 1,2,3 };
vector<int> v{ 1,2,3 };// 等號可以省略不寫
map<int, int> m{ {1,1},{2,2},{3,3} };

自定義型別的列表初始化:
下面是自己定義的一個類:

class Point
{
public:
	Point(int x, int y)
		:_x(x)
		,_y(y)
	{}
	void PrintPoint()
	{
		printf("點的座標為:(%d, %d)\n", _x, _y);
	}
private:
	int _x;
	int _y;
};

建立一個Point類並使用{}對其進行列表初始化,具體如下:

int main()
{
	// 自定義型別列表初始化  
	Point p{ 1, 2 };
	p.PrintPoint();
	size_t i = 0;
	
	return 0;
}

initializer_list

initializer_list容器沒有提供對應的增刪查改等介面,因為initializer_list並不是專門用於儲存資料的,而是為了讓其他容器支援列表初始化的。

initializer_list一般是作為建構函式的引數,C++11對STL中的不少容器就增加initializer_list作為引數的建構函式,這樣初始化容器物件就更方便了。也可以作為operator=的引數,這樣就可以用大括號賦值對應多個物件的列表初始化,必須支援一個帶有initializer_list型別引數的建構函式

注意: 這種型別的物件由編譯器根據初始化列表宣告自動構造,初始化列表宣告是用{}和,容器使用initializer_list作為建構函式的引數的例子

例項演示: 簡單模擬一個vector中的建構函式和賦值過載

template<class T>
class Vector
{
public:
	Vector(initializer_list<T> l)
		:_size(0)
		,_capacity(l.size())
	{
		_a = new T[_capacity];
		for (auto e : l)
		{
			_a[_size++] = e;
		}
	}
	Vector<T>& operator=(initializer_list<T> l)
	{
		delete _a;
		_size = 0;
		_capacity = l.size();
		_a = new T[_capacity];
		for (auto e : l)
		{
			_a[_size++] = e;
		}

		return *this;
	}
private:
	T* _a;
	size_t _size;
	size_t _capacity;
};

int main()
{
	Vector<int> v1 = { 1,2,3 };
	Vector<int> v2 = { 3,5,7,9 };
	v2 = { 1,2,3 };
	
	return 0;
}

補充:

  • C++98並不支援直接用列表對容器進行初始化,這種初始化方式是在C++11引入initializer_list後才支援的。
  • 這些容器之所以支援使用列表進行初始化,根本原因是因為C++11給這些容器都增加了一個建構函式,這個建構函式就是以initializer_list作為引數的。
  • 當用列表對容器進行初始化時,這個列表被識別成initializer_list型別,於是就會呼叫這個新增的建構函式對該容器進行初始化。
  • 這個新增的建構函式要做的就是遍歷initializer_list中的元素,然後將這些元素依次插入到要初始化的容器當中即可。
C++11(列表初始化+變數型別推導+型別轉換+左右值概念、引用+完美轉發和萬能應用+定位new+可變引數模板+emplace介面)

initializer_list使用示例:

  • 如果要讓vector支援列表初始化,就需要增加一個以initializer_list作為引數的建構函式。
  • 在建構函式中遍歷initializer_list時可以使用迭代器遍歷,也可以使用範圍for遍歷,因為範圍for底層實際採用的就是迭代器方式遍歷。
  • 使用迭代器方式遍歷時,需要在迭代器型別前面加上typename關鍵字,指明這是一個型別名字。因為這個迭代器型別定義在一個類别範本中,在該類别範本未被例項化之前編譯器是無法識別這個型別的
  • 最好也增加一個以initializer_list作為引數的賦值運算子過載函式,以支援直接用列表對容器物件進行賦值,但實際也可以不增加。
 
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		vector(initializer_list<T> il)
		{
			_start = new T[il.size()];
			_finish = _start;
			_endofstorage = _start + il.size();
 
			//迭代器遍歷
			//typename initializer_list<T>::iterator it = il.begin();
			//while (it != il.end())
			//{
			//	push_back(*it);
			//	it++;
			//}
 
			//範圍for遍歷
			for (auto e : il)
			{
				push_back(e);
			}
		}
 
		vector<T>& operator=(initializer_list<T> il)
		{
			vector<T> tmp(il);
			std::swap(_start, tmp._start);
			std::swap(_finish, tmp._finish);
			std::swap(_endofstorage, tmp._endofstorage);
			return *this;
		}
 
	private:
		iterator _start;
		iterator _finish;
		iterator _endofstorage;
	};

變數型別推導

auto型別推導

在C++98中auto是一個儲存型別的說明符,表明變數是區域性自動儲存型別,但是區域性域中定義區域性的變數預設就是自動儲存型別,所以auto就沒什麼價值了。C++11中廢棄auto原來的用法,將其用於實現自動型別推導。這樣要求必須進行顯示初始化,讓編譯器將定義物件的型別設定為初始化值的型別。

注意: auto宣告的型別必須要進行初始化,否則編譯器無法推匯出auto的實際型別。auto不能作為函式的引數和返回值,且不能用來直接宣告陣列型別。

int main()
{
	int a = 10;
	auto pa = &a;
	auto& ra = a;// 宣告引用型別要加&

	cout << typeid(a).name() << endl;
	cout << typeid(pa).name() << endl;
	cout << typeid(ra).name() << endl;

	return 0;
}

decltype型別推導

decltype是根據表示式的實際型別推演出定義變數時所用的型別。且還可以使用推匯出來的型別進行變數宣告

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int a = 10;
	int b = 20;

	// 用decltype自動推演a+b的實際型別
	decltype(a + b) c = 10;
	cout << typeid(c).name() << endl;
	// 不帶引數,推導函式型別
	cout << typeid(decltype(Add)).name() << endl;
	// 帶引數,推導函式返回值型別,注意這裡不會呼叫函式
	cout << typeid(decltype(Add(1,1))).name() << endl;

	return 0;
}

注意: decltype不可以作為函式的引數,編譯時推導型別

nullptr

①C++中NULL被定義成字面量0,這樣就可能會帶來一些問題,因為0既能表示指標常量,又能表示整型常量。所以出於清晰和安全的角度考慮,C++11中新增了nullptr,用於表示空指標

/* Define NULL pointer value */
#ifndef NULL
#ifdef __cplusplus
#define NULL    0
#else  /* __cplusplus */
#define NULL    ((void *)0)
#endif  /* __cplusplus */
#endif  /* NULL */

②在大部分情況下使用NULL不會存在什麼問題,但是在某些極端場景下就可能會導致匹配錯誤

  • NULL和nullptr的含義都是空指標,所以這裡呼叫函式時肯定希望匹配到的都是引數型別為int*的過載函式,但最終卻因為NULL本質是字面量0,而導致NULL匹配到了引數為int型別的過載函式,因此在C++中一般推薦使用nullptr
void f(int arg)
{
	cout << "void f(int arg)" << endl;
}
 
void f(int* arg)
{
	cout << "void f(int* arg)" << endl;
}
 
int main()
{
	f(NULL);    //void f(int arg)
	f(nullptr); //void f(int* arg)
	return 0;
}

型別轉換

C語言中的型別轉換

在C語言中,如果賦值運算子左右兩側型別不同,或者形參與實參型別不匹配,或者返回值型別與接收返回值型別不一致時就需要發生型別轉化,C語言中總共有兩種形式的型別轉換:隱式型別轉換和顯式型別轉換。

(1) 兩種形式的型別轉換

  • 隱式型別轉化:編譯器在編譯階段自動進行,能轉就轉,不能轉就編譯失敗
  • 顯式型別轉化:需要使用者自己處理
  • 型別轉換應為相近型別 : int, short, char之間就可以互相轉換,因為他們都是用來表示資料的大小,只是範圍不一樣;char因為編碼的原因用來表示字元,嚴格來說如果是有符號的話,它是從-128 - 127這個範圍的整數
void Test()
{
	int i = 1;
 
	// 隱式型別轉換
	double d = i;
	printf("%d, %.2f\n", i, d);
 
	// 顯示的強制型別轉換
	int* p = &i;
	int address = (int)p;
 
	printf("%x, %d\n", p, address);
}

C語言型別轉換的缺陷 : 轉換的可視性比較差,所有的轉換形式都是以一種相同形式書寫,難以跟蹤錯誤的轉換

C++四種型別轉換

C風格的轉換格式很簡單,但是有不少缺點的:

  • ①隱式型別轉化有些情況下可能會出問題:比如資料精度丟失
  • ② 顯式型別轉換將所有情況混合在一起,程式碼不夠清晰

因此C++提出了自己的型別轉化風格,注意因為C++要相容C語言,所以C++中還可以使用C語言的轉化風格

C++強制型別轉換

(1)static_cast

  • static_cast用於非多型型別的轉換(靜態轉換),編譯器隱式執行的任何型別轉換都可用static_cast,但它不能用於兩個不相關的型別進行轉換
int main()
{
	double d = 12.34;
	int a = static_cast<int>(d); //相近型別轉換
	cout << a << endl;
 
	int *p = static_cast<int*>(a);  //不相近型別轉換  -- 報錯
 
	return 0;
}

(2)reinterpret_cast

  • reinterpret_cast運算子通常為運算元的位模式提供較低層次的重新解釋,用於將一種型別轉換為另一種不同的型別
int main()
{
 double d = 12.34;
 int a = static_cast<int>(d);
 cout << a << endl;
 
 // 這裡使用static_cast會報錯,應該使用reinterpret_cast
 //int *p = static_cast<int*>(a);
 int *p = reinterpret_cast<int*>(a); //不相近型別轉換
 
 return 0; 
}

(3)const_cast

  • const_cast最常用的用途就是刪除變數的const屬性,方便賦值
  • C++把這個單獨分出來,意思提醒用的人注意,const屬性被去掉以後,會被修改。小心跟編譯器最佳化衝突誤判
void Test1()
{
	//const修飾的叫常變數
	const int a = 2;
	//a = 10;  //a不可修改
 
	//const int* p = &a; //還是不能改變
	// *p = 10;
 
	 int* p = const_cast<int*>(&a);  //const_cast用於去掉const屬性
	 *p = 10; //可修改
 
	cout << a << endl;  //2 從暫存器中取
	cout << *p << endl; //10  從記憶體中取
}

(4)dynamic_cast

①dynamic_cast用於將一個父類物件的指標/引用轉換為子類物件的指標或引用(動態轉換)

  • 向上轉型:子類物件指標/引用 -> 父類指標/引用(不需要轉換,賦值相容規則)
  • 向下轉型:父類物件指標/引用 -> 子類指標/引用(用dynamic_cast轉型是安全的)

②注意:

  • dynamic_cast只能用於含有虛擬函式的類 ,因為執行時型別檢查需要執行時的型別資訊,而這個資訊是儲存在虛擬函式表中的,只有定義了虛擬函式的類才有虛擬函式表
  • dynamic_cast會先檢查是否能轉換成功,能成功則轉換,不能則返回0

③測試用例

  • pa有兩種情況 :pa指向父類物件,pa指向子類物件
  • 如果pa是指向父類物件,那麼不做任何處理
  • 如果pa是指向子類物件,那麼請轉回子類,並訪問子類物件中_b成員
    dynamic_cast :如果pa指向的父類物件,那麼則轉換不成功,返回nullptr; 如果
  • pa指向的子類物件,那麼則轉換成功,返回物件指標
  • dynamic_cast會先檢查是否能轉換成功,能成功則轉換,不能則返回
class A
{
	virtual void f() {}
public:
};
 
class B : public A
{
public:
	int _b = 0;
};
 
void func(A* pa)
{
	B* pb1 = static_cast<B*>(pa);
	B* pb2 = dynamic_cast<B*>(pa);
	if (pb2 == nullptr)
	{
		cout << "轉換失敗!" << endl;
	}
	else
	{
		pb2->_b++;
	}
 
	cout << "pb1:" << pb1 << endl;
	cout << "pb2:" << pb2 << endl;
}
 
int main()
{
	A aa;
	B bb;
	func(&aa);
	func(&bb);
}
C++11(列表初始化+變數型別推導+型別轉換+左右值概念、引用+完美轉發和萬能應用+定位new+可變引數模板+emplace介面)

explicit

  • explicit用來修飾建構函式,從而禁止單引數建構函式的隱式轉換
class A
{
public:
	explicit A(int a)
	{
		cout << "A(int a)" << endl;
	}
	A(const A& a)
	{
		cout << "A(const A& a)" << endl;
	}
private:
	int _a;
};
 
int main()
{
	A a1(1);
	//A a2 = 1; //error
	return 0;
}

①在語法上,程式碼中的A a2 = 1等價於以下兩句 :

A tmp(1);  //構造
A a2(tmp); //複製構造     

②在早期的編譯器中,當編譯器遇到A a2 = 1這句程式碼時,會先構造一個臨時物件,再用這個臨時物件複製構造a2。但是現在的編譯器已經做了最佳化,當遇到A a2 = 1這句程式碼時,會直接按照A a2(1)的方式進行處理,這也叫做隱式型別轉換

但對於單引數的自定義型別來說,A a2 = 1這種程式碼的可讀性不是很好,因此可以用explicit修飾單引數的建構函式,從而禁止單引數建構函式的隱式轉換

右值引用和移動語義

左值VS右值

在之前的部落格中,我已經介紹過了引用的語法,那裡的引用都是左值引用。C++11新增了右值引用的語法特性,給右值取別名。左值引用和右值引用都是給物件取別名,只不過二者物件的特性不同

注意: 左值引用用符號&,右值引用的符號是&&

專有名詞 概念
左值 一個表示資料的表示式,可以取地址和賦值,且左值可以出現在賦值符號的左邊,也可以出現在賦值符號的右邊,例如:普通變數、指標等,const修飾後的左值不可以賦值,但是可以取地址,所以還是左值
左值引用 給左值的引用,給左值取別名 ,例如:int& ra = a;
右值 一個表示資料的表示式,右值不能取地址,右值可以出現在賦值符號的右邊,但是不能出現出現在賦值符號的左邊如:字面常量、表示式返回值,函式返回值(這個不能是左值引用返回)等等
右值引用 給右值的引用,給右值取別名,例如:int&& ra = Add(a,b)

例項1:

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int a = 10;// 左值,可以取地址
	int& ra = a;// 左值引用

	int&& ret = Add(3, 4);// 函式的返回值是一個臨時變數,是一個右值

	return 0;
}

總結:

左值

  • 普通型別的變數,可以取地址

  • const修飾的常量,可以取地址,也是左值

  • 如果表示式執行結果或單個變數是一個引用,則認為是右值

右值(本質就是一個臨時變數或常量值)

  • 純右值:基本型別的常量或臨時物件,如:a+b,字面常量
  • 將亡值:自定義型別的臨時物件用完自動完成析構,如:函式以值的方式返回一個物件
  • 這些純右值和將亡值並沒有被實際儲存起來,這也就是為什麼右值不能被取地址的原因,因為只有被儲存起來後才有地址

左值引用VS右值引用

傳統的C++語法中就有引用的語法,而C++11中新增了右值引用的語法特性,為了進行區分,於是將C++11之前的引用就叫做左值引用。但是無論左值引用還是右值引用,本質都是給物件取別名。

①左值引用就是對左值的引用,給左值取別名,透過“&”來宣告

int main()
{
	//以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;
 
	//以下幾個是對上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
	return 0;
}

②右值引用就是對右值的引用,給右值取別名,透過“&&”來宣告

int main()
{
	double x = 1.1, y = 2.2;
	
	//以下幾個都是常見的右值
	10;
	x + y;
	fmin(x, y);
 
	//以下幾個都是對右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	return 0;
}

③需要注意的是,右值是不能取地址的,但是給右值取別名後,會導致右值被儲存到特定位置,這時這個右值可以被取到地址,並且可以被修改,如果不想讓被引用的右值被修改,可以用const修飾右值引用。

int main()
{
	double x = 1.1, y = 2.2;
	int&& rr1 = 10;
	const double&& rr2 = x + y;
 
	rr1 = 20;
	rr2 = 5.5; //報錯
	return 0;
}

④左值引用總結

  • 左值引用不能引用右值,因為這涉及許可權放大的問題,右值是不能被修改的,而左值引用是可以修改。
  • 但是const左值引用可以引用右值,因為const左值引用能夠保證被引用的資料不會被修改。
  • 因此const左值引用既可以引用左值,也可以引用右值
int main()
{
	int a = 10;
	int& r1 = a;// 左值引用
	
	//int& r2 = 10;// error,左值引用不可以引用右值  (這是因為許可權放大了,所以不行)
	const int& r2 = 10;// const的左值引用可以引用右值(這是因為許可權不變,所以可以)

	return 0;
}

⑤右值引用總結

  • 右值引用只能引用右值,不能引用左值。
  • 但是右值引用可以引用move以後的左值
  • move函式是C++11標準提供的一個函式,被move後的左值能夠賦值給右值引用。
int main()
{
	int&& r1 = 10;// 對字面常量進行引用(右值引用)
	r1 = 20;// 10原本是一個字面常量,無空間儲存,右值引用後會開一塊空間把值存起來,可以取地址
	cout << &r1 << endl;

	int a = 10;
	// int&& r2 = a;   // error,右值引用不可以引用左值
	int&& r2 = move(a);// move後的左值可以引用,a的屬性不變,引用的是move的返回值

	return 0;
}

右值引用的移動語義

移動語義: 將一個物件中資源移動到另一個物件中的方式,可以有效減少複製,減少資源浪費,提供效率

問題提出:
先看下面簡單的移動程式碼:

class String
{
public:
	String(const char* str = "")
		:_str(new char[strlen(str) + 1])
		, _size(0)
	{
		strcpy(_str, str);
		_str[_size] = '\0';
	}
	String(const String& s)
		:_str(new char[strlen(s._str) + 1])
		, _size(s._size)
	{
		cout << "深複製" << endl;
		strcpy(_str, s._str);
	}
	String& operator=(String& s)
	{
		if (this != &s)
		{
			cout << "深複製" << endl;
			delete _str;
			_str = new char[strlen(s._str) + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_str[_size] = '\0';
		}

		return *this;
	}
	~String()
	{
		delete _str;
	}
private:
	char* _str;
	size_t _size;
};
String func(String& str)
{
	String tmp(str);
	return tmp;
}
int main()
{
	String s1("123");// 
	String s2(s1);
	String s3(func(s1));

	return 0;
}
/*
輸出:
深複製
深複製
深複製
*/

分析結果:
第一次深複製是因為s1複製構造s2,這裡都不難理解。主要看後兩次,s1傳參過程不發生深複製,因為這裡是傳引用,接著就是str複製構造tmp,這裡會發生一次深複製,緊接著就是返回tmp,tmp會先複製構造一個臨時物件(這裡會發生一次深複製),然後臨時物件複製構造給s3(這裡會發生一次深複製),連續兩次複製構造會被編譯器最佳化成一次,這也就是我們上面看到的兩次深複製
分析問題:
在上面的程式碼中,可以發現,func中的tmp、返回是構造的臨時物件和s3都有一個獨立的空間,且內容是相同的,這裡相當於建立了3個內容完全相同的物件,這是一種極大的浪費,且效率也會降低

如何解決?
移動語義來解決

C++11(列表初始化+變數型別推導+型別轉換+左右值概念、引用+完美轉發和萬能應用+定位new+可變引數模板+emplace介面)

這裡解決其實就是進行資源的轉移,這裡加上一份移動構造的程式碼,如下:

String(String&& s)
:_str(s._str)
{
	// 對於將亡值,內部做移動複製
	cout << "移動複製" << endl;
	s._str = nullptr;
}
/*
輸出結果:
深複製
深複製
移動複製
*/

因為返回的臨時物件是一個右值,所以會呼叫上面的移動構造的程式碼對返回的臨時物件進行構造,本質是資源進行轉移,此時tmp指向的是一塊空的資源。最後返回的臨時物件複製構造給s3時還是呼叫了移動構造,兩次移動構造被編譯器最佳化為一個。可以看出的是這裡解決的是減少接受函式返回物件時帶來的複製,極大地提高了效率

除了移動構造,我們還可以增加移動賦值,具體如下:

String& operator=(String&& s)
{
	cout << "移動賦值" << endl;
	_str = s._str;
	_size = s._size;
	s._str = nullptr;

	return *this;
}

演示:

int main()
{
	String s1("123");
	String s2("ABC");
	s2 = func(s1);

	return 0;
}
/*
輸出結果:
深複製
移動複製
移動賦值
*/

可以看出的是func返回的臨時物件是透過移動賦值給s2的,也是一次資源的轉義
注意:

  • 移動構造和移動賦值函式的引數千萬不能設定成const型別的右值引用,因為資源無法轉移而導致移動語義失效。
  • 在C++11中,編譯器會為類預設生成一個移動構造和移動賦值,該移動構造和移動賦值為淺複製,因此當類中涉及到資源管理時,使用者必須顯式定義自己的移動構造和移動賦值。

總結: 右值引用本身沒有多大意義,引入了移動構造和移動賦值就有意義了

右值引用和左值引用減少複製的場景:

  • 作引數時: 左值引用減少傳參過程中的複製。右值引用解決的是傳參後,函式內部的複製構造
  • 作返回值時: 如果出了函式作用域,物件還存在,那麼可以使用左值引用減少複製。如果不存在,那麼產生的臨時物件可以透過右值引用提供的移動構造生成,然後透過移動賦值或移動構造的方式將臨時物件的資源給接受返回值者

move

move: 當需要用右值引用引用一個左值時,可以透過move來獲得繫結到左值上的右值引⽤。C++11中,std::move()函式位於標頭檔案中,該函式名字具有迷惑性,它並不搬移任何東西,唯一的功能就是將一個左值強制轉化為右值引用,然後實現移動語義

move函式的定義

template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
	//forward _Arg as movable
	return ((typename remove_reference<_Ty>::type&&)_Arg);
}
  • move函式中_Arg引數的型別不是右值引用,而是萬能引用。萬能引用跟右值引用的形式一樣,但是右值引用需要是確定的型別。
  • 一個左值被move以後,它的資源可能就被轉移給別人了,因此要慎用一個被move後的左值。

注意:

  • 被轉化的左值,其生命週期並沒有隨著左值的轉化而改變,即std::move轉化的左值變數value不會被銷燬。move告訴編譯器:我們有⼀個左值,但我們希望像⼀個右值⼀樣處理它
  • STL中也有另一個move函式,就是將一個範圍中的元素搬移到另一個位置

使用如下:

int main()
{
	String s1("123");
	// 這裡我們把s1 move處理以後, 會被當成右值,呼叫移動構造
	// 但是這裡要注意,一般是不要這樣用的,因為我們會發現s1的
 	// 資源被轉移給了s2,s1被置空了
	String s2(move(s1));

	return 0;
}
//輸出:移動複製

需要知道的是,move後的s1變成了一個右值,所以會呼叫移動構造去構造s2,這樣s1的資源就被轉移給了s2,s1本身也沒有資源了

STL增加了右值引用

列舉一部分:

  • list的尾插
void push_back(const value_type& val);
void push_back(value_type&& val);
  • vector的尾插
void push_back(const value_type& val);
void push_back(value_type&& val);

如果要插入的物件是一個純右值或將亡值,就會呼叫下面這個版本的插入,如果為左值就會呼叫上面這個版本的插入

完美轉發和萬能引用

  • 完美轉發:是指在函式模板中,完全依照模板的引數的型別,將引數傳遞給函式模板中呼叫的另外一個函式。
  • 萬能引用: 模板中的&&不代表右值引用,而是萬能引用,其既能接收左值又能接收右值,但是引用型別的唯一缺點就是限制了接收的型別,後續使用中都退化成了左值。

問題: 右值引用的物件再作為實參傳遞時,屬性會退化為左值,只能匹配左值引用(右值引用後可以取地址,底層會開一塊空間把這樣的值存起來,所以屬性發生了改變)
如下:由於PerfectForward函式的引數型別是萬能引用,因此既可以接收左值也可以接收右值,而我們在PerfectForward函式中呼叫Fun函式,就是希望呼叫PerfectForward函式時傳入左值、右值、const左值、const右值,能夠匹配到對應版本的Fun函式

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// std::forward<T>(t)在傳參的過程中保持了t的原生型別屬性。
template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}
int main()
{
	PerfectForward(10); // 右值
	int a;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b); // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}
/*
輸出結果:屬性丟失
左值引用
左值引用
左值引用
const 左值引用
const 左值引用
*/
  • 但實際呼叫PerfectForward函式時傳入左值和右值,最終都匹配到了左值引用版本的Func函式,呼叫PerfectForward函式時傳入const左值和const右值,最終都匹配到了const左值引用版本的Func函式。
  • 根本原因就是,右值被引用後會導致右值被儲存到特定位置,這時這個右值可以被取到地址,並且可以被修改,所以在PerfectForward函式中呼叫Func函式時會將t識別成左值。
  • 也就是說,右值經過一次引數傳遞後其屬性會退化成左值,如果想要在這個過程中保持右值的屬性,就需要用到完美轉發

解決:要想在引數傳遞過程中保持其原有的屬性,需要在傳參時呼叫forward函式,使用完美轉發能夠在傳遞過程中保持它的左值或者右值的屬性

template<typename T>
void PerfectForward(T&& t)
{
	Fun(std::forward<T>(t));
}
/*
輸出結果:屬性保持了
右值引用
左值引用
右值引用
const 左值引用
const 右值引用
*/

總結: 右值引用在傳參的過程中移動要進行完美轉發,否則會丟失右值屬性

完美轉發的使用場景

①模擬實現了一個簡化版的list類,類當中分別提供了左值引用版本和右值引用版本的push_back和insert函式

namespace XM
{
	template<class T>
	struct ListNode
	{
		T _data;
		ListNode* _next = nullptr;
		ListNode* _prev = nullptr;
	};
 
	template<class T>
	class list
	{
		typedef ListNode<T> node;
	public:
		//建構函式
		list()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
		}
 
		//左值引用版本的push_back
		void push_back(const T& x)
		{
			insert(_head, x);
		}
 
		//右值引用版本的push_back
		void push_back(T&& x)
		{
			insert(_head, std::forward<T>(x)); //完美轉發
		}
 
		//左值引用版本的insert
		void insert(node* pos, const T& x)
		{
			node* prev = pos->_prev;
			node* newnode = new node;
			newnode->_data = x;
 
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = pos;
			pos->_prev = newnode;
		}
 
		//右值引用版本的insert
		void insert(node* pos, T&& x)
		{
			node* prev = pos->_prev;
			node* newnode = new node;
			newnode->_data = std::forward<T>(x); //完美轉發
 
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = pos;
			pos->_prev = newnode;
		}
	private:
		node* _head; //指向連結串列頭結點的指標
	};
}
 
 
int main()
{
	XM::list<XM::string> lt;
	XM::string s("1111"); 
	lt.push_back(s);      //呼叫左值引用版本的push_back
 
	lt.push_back("2222"); //呼叫右值引用版本的push_back
	return 0;
}

②呼叫左值引用版本的push_back函式插入元素時,會呼叫string原有的operator=函式進行深複製;呼叫右值引用版本的push_back函式插入元素時,只會呼叫string的移動賦值進行資源的移動。

  • 因為實現push_back函式時複用了insert函式的程式碼,對於左值引用版本的push_back函式,在呼叫insert函式時只能呼叫左值引用版本的insert函式,而在insert函式中插入元素時會先new一個結點,然後將對應的左值賦值給該結點,因此會呼叫string原有的operator=函式進行深複製。
  • 而對於右值引用版本的push_back函式,在呼叫insert函式時就可以呼叫右值引用版本的insert函式,在右值引用版本的insert函式中也會先new一個結點,然後將對應的右值賦值給該結點,因此這裡就和呼叫string的移動賦值函式進行資源的移動。
  • 這個場景中就需要用到完美轉發,否則右值引用版本的push_back接收到右值後,該右值的右值屬性就退化了,此時在右值引用版本的push_back函式中呼叫insert函式,也會匹配到左值引用版本的insert函式,最終呼叫的還是原有的operator=函式進行深複製。
  • 此外,除了在右值引用版本的push_back函式中呼叫insert函式時,需要用完美轉發保持右值原有的屬性之外,在右值引用版本的insert函式中用右值給新結點賦值時也需要用到完美轉發,否則在賦值時也會將其識別為左值,導致最終呼叫的還是原有的operator=函式。
    也就是說,只要想保持右值的屬性,在每次右值傳參時都需要進行完美轉發,實際STL庫中也是透過完美轉發來保持右值屬性的。

③程式碼中push_back和insert函式的引數T&&是右值引用,而不是萬能引用,因為在list物件建立時這個類就被例項化了,後續呼叫push_back和insert函式時,引數T&&中的T已經是一個確定的型別了,而不是在呼叫push_back和insert函式時才進行型別推導的。

與STL中的list的區別

①將剛才測試程式碼中的list換成STL當中的list

呼叫左值引用版本的push_back插入結點,在構造結點時會呼叫string的複製建構函式。
呼叫右值引用版本的push_back插入結點,在構造結點時會呼叫string的移動建構函式。
而用我們模擬實現的list時,呼叫的卻不是string的複製構造和移動構造,而對應是string原有的operator=和移動賦值。

②我們模擬實現的list容器,是透過new運算子為新結點申請記憶體空間的,在申請記憶體後會自動呼叫建構函式對進行其進行初始化,因此在後續用左值或右值對其進行賦值時,就會呼叫對應的 operator= 或移動賦值 進行深複製或資源的轉移。

C++11(列表初始化+變數型別推導+型別轉換+左右值概念、引用+完美轉發和萬能應用+定位new+可變引數模板+emplace介面)

③STL庫中的容器都是透過空間配置器獲取記憶體的,因此在申請到記憶體後不會呼叫建構函式對其進行初始化,而是後續用左值或右值對其進行複製構造,因此最終呼叫的就是複製構造或移動構造。

  • 如果想要得到與STL相同的實驗結果,可以使用malloc函式申請記憶體,這時就不會自動呼叫建構函式進行初始化,然後在用定位new(下面會介紹)的方式用左值或右值對申請到的記憶體空間進行構造,這時呼叫的對應就是複製構造或移動構造。
C++11(列表初始化+變數型別推導+型別轉換+左右值概念、引用+完美轉發和萬能應用+定位new+可變引數模板+emplace介面)

定位new

定位new表示式是在已分配的原始記憶體空間中呼叫建構函式初始化一個物件。換句話說就是,現在空間已經有了,不需要定位new常規new一樣去給申請空間,只需要定位new在已有的空間上呼叫建構函式構造物件而已。

定位new的使用格式:

1.new (place_address) type 
2.new (palce_address) type (initializer_list)

用法1與用法2的區別在於物件是否需要初始化,其中place_address必須是一個指標,initializer_listtype型別的初始化列表。

注意事項:

#include <iostream>
#include <string>
#include <new>
using namespace std;

const int BUF = 512;
class JustTesting {
private:
	string words;
	int number;
public:
	JustTesting(const string& s = "Just Testing", int n = 0) {
		words = s;
		number = n;
		cout << words << " constructed" << endl;
	}
	~JustTesting() { cout << words << " destroyed!" << endl; }
	void Show()const { cout << words << " , " << number << endl; }
};

int main() {
	char* buffer = new char[BUF];//常規new在堆上申請空間

	JustTesting* pc1, * pc2;

	pc1 = new (buffer) JustTesting;//定位new
	pc2 = new JustTesting("Heap1", 20);//常規new

	cout << "Memory block address:\n" << "buffer: " << (void*)buffer << " heap: " << pc2 << endl;
	cout << "Memory contents:\n" << pc1 << ": ";
	pc1->Show();
	cout << pc2 << ": ";
	pc2->Show();

	JustTesting* pc3, * pc4;
	pc3 = new (buffer)JustTesting("Bad Idea", 6);//定位new
	pc4 = new JustTesting("Heap2", 10);//常規new

	cout<< "Memory contents:\n" << pc3 << ": ";
	pc3->Show();
	cout << pc4 << ": ";
	pc4->Show();

	delete pc2;//釋放pc2申請的空間
	delete pc4;//釋放pc4申請的空間
	delete[] buffer;//釋放buffer指向的空間
	cout << "Done!" << endl;
	return 0;
}

該例程首先使用常規new建立了一塊512位元組的記憶體緩衝區(buffer指向),然後使用new在堆上建立兩個JustTesting物件,並且嘗試使用定位new在buffer指向的記憶體緩衝區上建立兩個JustTesting物件。最後使用delete釋放new分配的記憶體。以下是該例程的執行結果:
C++11(列表初始化+變數型別推導+型別轉換+左右值概念、引用+完美轉發和萬能應用+定位new+可變引數模板+emplace介面)

我們一步一步分析以下結果:

1.Justing Testing constructed是由定位new在記憶體緩衝區上構造第一個物件時,呼叫建構函式引發的結果;
2.Heap1 constructed是常規new自己在堆上申請空間,構造第二個物件呼叫建構函式引發的結果
3.Memory block address:是main函式中第一個cout的結果
4.buffer:00D117C0 heap:00D0EE98是main函式中第一個cout的結果,表明第一個JustTesting物件與第二個JustTesting物件不在同一記憶體區域
5.Memory contents:是main函式中第二個cout的結果
6.00D117C0: Just Testing , 0:第三個cout及pc1->Show()的結果,可以發現pc1就是記憶體緩衝區的地址
7.00D0EE98: Heap1 , 20:第四個cout及pc2->Show()的結果
8.Bad Idea constructed:是由定位new在記憶體緩衝區上構造第三個物件時,呼叫建構函式引發的結果;
9.Heap2 constructed:是常規new自己在堆上申請空間,構造第四個物件呼叫建構函式引發的結果
10.Memory contents:第五個cout的結果
11.00D117C0: Bad Idea , 6:第六個cout及pc3->Show()的結果,可以發現pc3就是記憶體緩衝區的地址
12.00D0F078: Heap2 , 10:第七個cout及pc4->Show()的結果
13.Heap1 destroyed!:delete pc2引發第二個物件的解構函式引發
14.Heap2 destroyed!:delete pc4引發第四個物件的解構函式引發
15.Done!:最後一個cout的結果

由例程程式碼及結果分析可以看出:
1.使用定位new建立的物件的地址與記憶體緩衝區地址一致,說明定位new並沒有申請新空間,而建構函式的呼叫說明定位new的確呼叫了建構函式。
2.在使用delete回收空間時,可以發現並未回收pc1與pc3,其原因在於pc1與pc3指向的物件位於記憶體緩衝區,該空間並不是定位new申請,而是常規new申請的,因此我們需要delete[]回收記憶體緩衝區,而不是delete pc1與delete pc3
3.pc1與pc3一致,說明第一個物件被第三個覆蓋!顯然,如果類動態地為其成員分配記憶體,這將引發問題!,所以,當我們使用定位new建立物件必須自己保證不會覆蓋任何不想丟失的資料!,就這個例程而言,避免覆蓋,最簡單的做法如下:

pc1 = new (buffer) JustTesting;
pc3 = new (buffer + sizeof(JustTesting)) JustTesting("Better Idea!",6);

4.delete[] buffer並未引發物件的析構!,雖然物件1及3的空間被回收,但物件1與3並未析構!這一點將時刻提醒我們使用定位new需要自己顯式呼叫解構函式,完成物件的析構!,但該析構並不能透過delete pc1或delete pc3實現!(因為delete與定位new不能配合使用!,否則會引發執行時錯誤!),只能透過顯式析構,如下:

pc3->~JustTesting();
pc1->~JustTesting();

使用定位new建立物件,顯式呼叫解構函式是必須的,這是解構函式必須被顯式呼叫的少數情形之一!,另有一點!!!解構函式的呼叫必須與物件的構造順序相反!切記!!!

新增的兩個預設成員函式

在類和物件的部落格中,已經介紹了類的6個預設成員函式:

  1. 建構函式
  2. 解構函式
  3. 複製建構函式
  4. 複製賦值過載
  5. 取地址過載
  6. const 取地址過載

在C++11中由新增了兩個預設成員函式:

  • 移動建構函式
  • 移動賦值運算子過載

這兩個函式相信大家都不陌生,上面介紹右值引用中也介紹了這兩個函式,右值引用和這兩個函式結合使用才能夠彰顯出右值引用的實際意義。需要注意的幾點是:

  • 如果沒有實現移動建構函式,且沒有實現解構函式 、複製構造、複製賦值過載中的任意一個。那麼編譯器會自動生成一個預設移動構造。

  • 預設生成的移動建構函式,對於內建型別成員會按照位元組序進行淺複製,自定義型別成員,則需要看這個成員是否實現移動構造,如果實現了就呼叫移動構造,沒有實現就呼叫複製構造。

  • 如果沒有實現移動賦值過載函式,且沒有實現解構函式 、複製構造、複製賦值過載中的任意一個,那麼編譯器會自動生成一個預設移動賦值。

  • 預設生成的移動建構函式,對於內建型別成員會按照位元組序進行淺複製,自定義型別成員,則需要看這個成員是否實現移動賦值,如果實現了就呼叫移動賦值,沒有實現就呼叫複製賦值。

可以看出,想讓編譯器自動生成移動構造和移動賦值要求還是很嚴格的。

預設生成的移動構造和移動賦值會做什麼

  • 預設生成的移動建構函式:對於內建型別的成員會完成值複製(淺複製),對於自定義型別的成員,如果該成員實現了移動構造就呼叫它的移動構造,否則就呼叫它的複製構造。
  • 預設生成的移動賦值過載函式:對於內建型別的成員會完成值複製(淺複製),對於自定義型別的成員,如果該成員實現了移動賦值就呼叫它的移動賦值,否則就呼叫它的複製賦值。

例項演示: 為了方便觀察,這裡我使用自己簡單模擬實現的string來進行演示。拿解構函式做演示,有解構函式和沒有解構函式,兩種情況下,使用右值物件構造一個物件和使用右值物件給一個物件賦值,觀察會呼叫哪個函式

namespace XM
{
	class string
	{
	public:
		//建構函式
		string(const char* str = "")
		{
			_size = strlen(str); //初始時,字串大小設定為字串長度
			_capacity = _size; //初始時,字串容量設定為字串長度
			_str = new char[_capacity + 1]; //為儲存字串開闢空間(多開一個用於存放'\0')
			strcpy(_str, str); //將C字串複製到已開好的空間
		}
 
		//交換兩個物件的資料
		void swap(string& s)
		{
			//呼叫庫裡的swap
			::swap(_str, s._str); //交換兩個物件的C字串
			::swap(_size, s._size); //交換兩個物件的大小
			::swap(_capacity, s._capacity); //交換兩個物件的容量
		}
 
		//複製建構函式(現代寫法)
		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(const string& s) -- 深複製" << endl;
 
			string tmp(s._str); //呼叫建構函式,構造出一個C字串為s._str的物件
			swap(tmp); //交換這兩個物件
		}
 
		//移動構造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移動構造" << endl;
			swap(s);
		}
 
		//複製賦值函式(現代寫法)
		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 深複製" << endl;
 
			string tmp(s); //用s複製構造出物件tmp
			swap(tmp); //交換這兩個物件
			return *this; //返回左值(支援連續賦值)
		}
 
		//移動賦值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移動賦值" << endl;
			swap(s);
			return *this;
		}
 
		//解構函式
		~string()
		{
			//delete[] _str;  //釋放_str指向的空間
			_str = nullptr; //及時置空,防止非法訪問
			_size = 0;      //大小置0
			_capacity = 0;  //容量置0
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

簡單的Person類,Person類中的成員name的型別就是我們模擬實現的string類

class Person
{
public:
	//建構函式
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
 
	//複製建構函式
	Person(const Person& p)
		:_name(p._name)
		, _age(p._age)
	{}
 
	//複製賦值函式
	Person& operator=(const Person& p)
	{
		if (this != &p)
		{
			_name = p._name;
			_age = p._age;
		}
		return *this;
	}
 
	//解構函式
	~Person()
	{}
private:
	XM::string _name; //姓名
	int _age;         //年齡
};

Person類當中沒有實現移動構造和移動賦值,但複製構造、複製賦值和解構函式Person類都實現了,因此Person類中不會生成預設的移動構造和移動賦值

int main()
{
	Person s1("張三", 100);
	Person s2 = std::move(s1); //想要呼叫Person預設生成的移動構造
 
	return 0;
}
C++11(列表初始化+變數型別推導+型別轉換+左右值概念、引用+完美轉發和萬能應用+定位new+可變引數模板+emplace介面)
  • 上述程式碼中用一個右值去構造s2物件,但由於Person類沒有生成預設的移動建構函式,因此這裡會呼叫Person的複製建構函式(複製構造既能接收左值也能接收右值),這時在Person的複製建構函式中就會呼叫string的複製建構函式對name成員進行深複製。
  • 如果要讓Person類生成預設的移動建構函式,就必須將Person類中的複製構造、複製賦值和解構函式全部註釋掉,這時用右值去構造s2物件時就會呼叫Person預設生成的移動建構函式。
  • Person預設生成的移動構造,對於內建型別成員age會進行值複製,而對於自定義型別成員name,因為我們的string類實現了移動建構函式,因此它會呼叫string的移動建構函式進行資源的轉移。
  • 而如果我們將string類當中的移動建構函式註釋掉,那麼Person預設生成的移動建構函式,就會呼叫string類中的複製建構函式對name成員進行深複製。

驗證Person類中預設生成的移動賦值函式

int main()
{
	Person s1("張三", 100);
	Person s2;
	s2 = std::move(s1); //想要呼叫Person預設生成的移動賦值
 
	return 0;
}
C++11(列表初始化+變數型別推導+型別轉換+左右值概念、引用+完美轉發和萬能應用+定位new+可變引數模板+emplace介面)

兩個關鍵字——default和delete

C++11可以讓你更好的控制要使用的預設函式。你可以強制生成某個預設成員函式,也可以禁止生成某個預設成員函式,分別用的的關鍵字是——defaultdelete

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		,_age(age)
	{}
	Person(Person&& p) = default;// 強制生成預設的
	Person& operator=(Person&& p) = default;
	Person(Person& p) = delete;// 強制刪除預設的
	~Person()
	{}
private:
	Simulation::string _name;
	int _age;
};

可變引數模板

C++11的新特性可變引數模板能夠讓您建立可以接受可變引數的函式模板和類别範本,相比C++98/03,類模版和函式模版中只能含固定數量的模版引數,可變模版引數無疑是一個巨大的改進。

語法規範如下:

template <class ...Args>
void fun(Args ...args)
{}

說明幾點:

  • Args和args前面有省略號,所以它們是可變引數,帶省略號的引數稱為“引數包”,它裡面包含了0到N(N>=0)個模版引數
  • Args是一個模板引數包,args是一個函式形參引數包

如何獲取引數包中的每一個引數呢?下面介紹兩種方法:

我們無法直接獲取引數包中的每個引數,只能透過展開引數包的方式來獲取,這是使用可變引數模板的一個主要特點,也是最大的難點

  • 方法一:遞迴函式展開引數包

①遞迴展開引數包的方式如下

  • 給函式模板增加一個模板引數,這樣就可以從接收到的引數包中分離出一個引數出來。
  • 在函式模板中遞迴呼叫該函式模板,呼叫時傳入剩下的引數包。
  • 如此遞迴下去,每次分離出引數包中的一個引數,直到引數包中的所有引數都被取出來。

②列印呼叫函式時傳入的各個引數,這樣編寫函式模板

template<class T, class ...Args>
void ShowList(T value, Args ...args)
{
	cout << value << " ";
	// 遞迴呼叫ShowList,當引數包中的引數個數為0時,呼叫上面無參的ShowList
	// args中第一個引數作為value傳參,引數包中剩下的引數作為新的引數包傳參
	ShowList(args...);
}
int main()
{
	ShowList(1, 'A');
	ShowList(3, 'a', 1.23);
	ShowList('a', 4, 'B', 3.3);
	return 0;
}
/*
輸出結果:
1 A 
3 a 1.23
a 4 B 3.3
*/

③現在面臨的問題是,如何終止函式的遞迴呼叫

  • 我們可以在剛才的基礎上,編寫只有一個帶參的遞迴終止函式,該函式的函式名與展開函式的函式名相同,構成函式過載
  • 當遞迴呼叫ShowList函式模板時,如果傳入的引數包中引數的個數為1,那麼就會匹配到這一個引數遞迴終止函式,這樣就結束了遞迴。
  • 但是需要注意,這裡的遞迴呼叫函式需要寫成函式模板,因為我們並不知道最後一個引數是什麼型別的。
//遞迴終止函式
//可以認為是過載版本,當引數包裡面只有一個引數的時候就會呼叫這個過載函式
template<class T>
void ShowList(const T& val)
{
	cout << val << endl << endl;
}
//展開函式
template<class T, class ...Args>
void ShowList(T value, Args ...args)
{
	cout << value << " ";
	// 遞迴呼叫ShowList,當引數包中的引數個數為0時,呼叫上面無參的ShowList
	// args中第一個引數作為value傳參,引數包中剩下的引數作為新的引數包傳參
	ShowList(args...);
}
int main()
{
	ShowList(1, 'A');
	ShowList(3, 'a', 1.23);
	ShowList('a', 4, 'B', 3.3);
	return 0;
}
/*
輸出結果:
1 A 
3 a 1.23
a 4 B 3.3
*/
  • 方法二:逗號表示式展開引數包

①透過列表獲取引數包中的引數

  • 1.陣列可以透過列表進行初始化
int a[] = {1,2,3,4};
  • 2.如果引數包中各個引數的型別都是整型,那麼也可以把這個引數包放到列表當中初始化這個整型陣列,此時引數包中引數就放到陣列中了(如果一個引數包中都是一個的型別,那麼可以使用該引數包對陣列進行列表初始化,引數包會展開,然後對陣列進行初始化。)
//展開函式
template<class ...Args>
void ShowList(Args... args)
{
	int arr[] = { args... }; //列表初始化
 
	//列印引數包中的各個引數
	for (auto e : arr)
	{
		cout << e << " ";
	}
	cout << endl;
}
  • 4.C++並不像Python這樣的語言,C++規定一個容器中儲存的資料型別必須是相同的,因此如果這樣寫的話,那麼呼叫ShowList函式時傳入的引數只能是整型的,並且還不能傳入0個引數,因為陣列的大小不能為0,因此我們還需要在此基礎上藉助逗號表示式來展開引數包。

②透過逗號表示式展開引數包

  • 逗號表示式會從左到右依次計算各個表示式,並且將最後一個表示式的值作為返回值進行返回。
  • 將逗號表示式的最後一個表示式設定為一個整型值,確保逗號表示式返回的是一個整型值。
  • 將處理引數包中引數的動作封裝成一個函式,將該函式的呼叫作為逗號表示式的第一個表示式。

1.在執行逗號表示式時就會先呼叫處理函式處理對應的引數,然後再將逗號表示式中的最後一個整型值作為返回值來初始化整型陣列。

  • 我們這裡要做的就是列印引數包中的各個引數,因此處理函式當中要做的就是將傳入的引數進行列印即可。
  • 可變引數的省略號需要加在逗號表示式外面,表示需要將逗號表示式展開,如果將省略號加在args的後面,那麼引數包將會被展開後全部傳入PrintArg函式,程式碼中的{ (PrintArg(args), 0)... }將會展開成{ (PrintArg(arg1), 0), (PrintArg(arg2), 0), (PrintArg(arg3), 0), ... }。
template<class T>
void PrintArg(T value)
{
	cout << value << " ";
}
template<class ...Args>
void ShowList(Args ...args)
{
	int arr[] = { (PrintArg(args), 0)... };
	cout << endl;
}

2.這時呼叫ShowList函式時就可以傳入多個不同型別的引數了,但呼叫時仍然不能傳入0個引數,因為陣列的大小不能為0,如果想要支援傳入0個引數,也可以寫一個無參的ShowList函式。

//支援無參呼叫
void ShowList()
{
	cout << endl;
}
template<class T>
void PrintArg(T value)
{
	cout << value << " ";
}
template<class ...Args>
void ShowList(Args ...args)
{
	int arr[] = { (PrintArg(args), 0)... };//列表初始化+逗號表示式
	cout << endl;
}

emplace系列介面

C++11(列表初始化+變數型別推導+型別轉換+左右值概念、引用+完美轉發和萬能應用+定位new+可變引數模板+emplace介面) C++11(列表初始化+變數型別推導+型別轉換+左右值概念、引用+完美轉發和萬能應用+定位new+可變引數模板+emplace介面)

上面的&&是萬能引用,實參是左值,引數包的這個形參就是左值引用,實參是右值,引數包的這個形參就是右值引用

以list容器的emplace_back和push_back為例

  • 呼叫push_back函式插入元素時,可以傳入左值物件或者右值物件,也可以使用列表進行初始化。
  • 呼叫emplace_back函式插入元素時,也可以傳入左值物件或者右值物件,但不可以使用列表進行初始化。
  • 除此之外,emplace系列介面最大的特點就是,插入元素時可以傳入用於構造元素的引數包,emplace_back可以支援可變引數包,然後呼叫定位new,使用引數包對空間進行初始化。
int main()
{
	list<pair<int, string>> mylist;
	pair<int, string> kv(1, "A");
	mylist.push_back(kv);                           //傳左值
	mylist.push_back(pair<int, string>(1, "A"));            //傳右值
	mylist.push_back({ 1, "A" });                   //列表初始化
 
	mylist.emplace_back(kv);                        //傳左值
	mylist.emplace_back(pair<int, string>(1, "A"));         //傳右值
	mylist.emplace_back(1, "A");                    //傳引數包
	return 0;
}

emplace系列介面的工作流程

  • 先透過空間配置器為新結點獲取一塊記憶體空間,注意這裡只會開闢空間,不會自動呼叫建構函式對這塊空間進行初始化。
  • 然後呼叫allocator_traits::construct函式對這塊空間進行初始化,呼叫該函式時會傳入這塊空間的地址和使用者傳入的引數(需要經過完美轉發)。
  • 在allocator_traits::construct函式中會使用定位new表示式,顯示呼叫建構函式對這塊空間進行初始化,呼叫建構函式時會傳入使用者傳入的引數(需要經過完美轉發)。
  • 將初始化好的新結點插入到對應的資料結構當中,比如list容器就是將新結點插入到底層的雙連結串列中。

emplace系列介面的意義

(1)由於emplace系列介面的可變模板引數的型別都是萬能引用,因此既可以接收左值物件,也可以接收右值物件,還可以接收引數包

  • 如果呼叫emplace系列介面時傳入的是左值物件,那麼首先需要先在此之前呼叫建構函式例項化出一個左值物件,最終在使用定位new表示式呼叫建構函式對空間進行初始化時,會匹配到複製建構函式。
  • 如果呼叫emplace系列介面時傳入的是右值物件,那麼就需要在此之前呼叫建構函式例項化出一個右值物件,最終在使用定位new表示式呼叫建構函式對空間進行初始化時,就會匹配到移動建構函式。
  • 如果呼叫emplace系列介面時傳入的是引數包,那就可以直接呼叫函式進行插入,並且最終在使用定位new表示式呼叫建構函式對空間進行初始化時,匹配到的是建構函式。

(2)小結

  • 傳入左值物件,需要呼叫建構函式+複製建構函式。
  • 傳入右值物件,需要呼叫建構函式+移動建構函式。
  • 傳入引數包,只需要呼叫建構函式。

(3)emplace介面的意義

  • emplace系列介面最大的特點就是支援傳入引數包,用這些引數包直接構造出物件,這樣就能減少一次複製,這就是為什麼有人說emplace系列介面更高效的原因。
  • 但emplace系列介面並不是在所有場景下都比原有的插入介面高效,如果傳入的是左值物件或右值物件,那麼emplace系列介面的效率其實和原有的插入介面的效率是一樣的。
  • emplace系列介面真正高效的情況是傳入引數包的時候,直接透過引數包構造出物件,避免了中途的一次複製。

相關文章