国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』

這篇具有很好參考價值的文章主要介紹了C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

?個人主頁: 北 海
??所屬專欄: C++修行之路
??操作環(huán)境: Visual Studio 2022 版本 17.6.5

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言



??前言

自從C++98以來,C++11無疑是一個相當(dāng)成功的版本更新。它引入了許多重要的語言特性和標(biāo)準(zhǔn)庫增強(qiáng),為C++編程帶來了重大的改進(jìn)和便利。C++11的發(fā)布標(biāo)志著C++語言的現(xiàn)代化和進(jìn)步,為程序員提供了更多工具和選項來編寫高效、可維護(hù)和現(xiàn)代的代碼

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言


???正文

1.右值引用

右值引用C++11 的重大更新之一,它的出現(xiàn)很好的解決了 臨時資源浪費 的問題,同時也給 類和對象 做了一個全面升級,使其能輕松規(guī)避很多低效拷貝問題

1.1.什么是右值引用?

在學(xué)習(xí) 右值引用 之前,需要先來看看 左值引用引用C++ 相對于 C語言 的升級點之一,引用 既能像指針那樣獲取資源的地址,直接對資源進(jìn)行操縱,也不必?fù)?dān)心多重 引用 問題,對于絕大多數(shù)場景來說,引用指針 好用得多

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

而我們之前使用的所有引用都稱為 左值引用,主要用于引用各種 變量,如果想引用 常量,需要使用 const 修飾

// 左值引用
int main()
{
	int a = 10;

	// 引用變量
	int& ra = a;

	// 引用 常量/臨時對象
	const int& rb = 10;
	const int& rc = int();
	return 0;
}

C++11 中,新增了 右值引用 的概念,就是將 左值引用 中的 & 變?yōu)?&&,右值引用 可以直接引用 左值引用 中需要加 const 引用的值;也可以通過函數(shù) move 引用 左值引用 直接引用的值

// 右值引用
int main()
{
	int a = 10;

	// 引用 常量/臨時對象
	int&& rrb = 10;
	int&& rrc = int();

	// 引用變量
	int&& ra = move(a);
	return 0;
}

其中,諸如 「變量 / 數(shù)組元素 / 解引用后的指針」 等,在表達(dá)式結(jié)束后仍然存在、并且可以被取地址的值稱為 左值;而 「常量 / 臨時對象 / 表達(dá)式結(jié)果」 等,在表達(dá)式結(jié)束后即將被銷毀的臨時對象,或者無法被直接取地址的值稱為 右值

快速判斷 左值 / 右值 的方法之一就是 看看能不能取地址

// 判斷左值 / 右值
int main()
{
	int a = 10;

	// 左值
	cout << &a << endl;

	// 右值
	cout << &10 << endl; // 【報錯】
	cout << &int() << endl; // 【報錯】
	return 0;
}

直接可以引用 左值 的稱為 左值引用,直接可以引用 右值 的就是 右值引用

注意:

  1. 左值引用 可以通過其他手段引用 右值,比如加 const;右值引用 也可以通過其他手段引用 左值,比如 move 函數(shù)
  2. 賦值語句左邊的一定是 左值,但右邊的不一定是 右值,比如 int a = b

1.2.move 轉(zhuǎn)移資源

無論是 左值引用 還是 右值引用,本質(zhì)上都是在給 資源 起別名,當(dāng) 左值引用 引用 左值 時,是直接指向 資源,從而對 左值 進(jìn)行操作;當(dāng) 右值引用 引用 右值 時,則是先將 常量 等即將被銷毀的臨時資源 “轉(zhuǎn)移” 到特定位置,然后指向該位置中的 資源,對 右值 進(jìn)行操作

int a = 10;

// 左值引用 引用 左值
int& ra = a;

// 右值引用 引用 右值
int&& rr = 10;

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

正因為將資源 “轉(zhuǎn)移” 了,右值引用 才可以對資源進(jìn)行利用

所以雖然 右值引用 引用的是 右值,但 右值引用 本身是可以取地址的,比如 &rr 是可以的,畢竟 rr 也指向了一塊空間,這塊空間中存儲的是臨時資源,這也就意味著 右值引用 是可以對臨時資源進(jìn)行修改操作的,也就是將臨時資源再利用

對于 「常量 / 臨時對象 / 表達(dá)式結(jié)果」右值,編譯器會直接轉(zhuǎn)移資源,但對于用戶自定義的 左值,編譯器不敢輕舉妄動,只敢給用戶提供一個 轉(zhuǎn)移變量資源 的函數(shù) move,有了 move 之后,右值引用 就能引用 左值

int a = 10;

// 左值引用 引用 右值
const int& r = 10;

// 右值引用 引用 左值
int&& rr = move(a);

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

語法還支持給 右值引用const,這樣做的含義是 不能修改右值引用后的值

int main() 
{
	int a = 10;

	const int&& crr = 10;
	const int&& crra = move(a);

	++crr; // 【報錯】
	++crra; // 【報錯】

	return 0;
}

一般情況下是不會這樣干的,右值引用 是為了移走資源,加了 const 還不如直接改用 const 左值引用


不要輕易使用 move 函數(shù),左值 中的資源可能會被轉(zhuǎn)走,在 C++11 之后,幾乎所有的 STL 容器都增加了一個 移動構(gòu)造 函數(shù),其中就用到了 右值引用

如果此時我們直接將 左值 move 后構(gòu)造一個新對象,會導(dǎo)致原本左值中的 資源 丟失

// move 轉(zhuǎn)移資源
int main()
{
	string str = "Hello World!";
	cout << "str: " << str << endl;

	// 使用 move 函數(shù)后
	string tmp = move(str);
	cout << "str: " << str << endl;

	return 0;
}

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

所以一般情況下不要輕易使用 move 移動函數(shù),除非你確定該資源后續(xù)不再使用

1.3.左值引用 vs 右值引用

C++11 之前,使用 const 左值引用 也可以引用 右值,并且在我們之前的學(xué)習(xí)中只使用 左值引用 也沒什么大問題啊,那為什么還要搞出一個 右值引用 呢?

答案是 右值引用可以提高資源的利用率,進(jìn)而提高整體效率

有了右值引用之后,之前只能 【讀取】、【拷貝】的臨時資源變得更有價值了,可以在右值引用后進(jìn)行操作,也可以將資源轉(zhuǎn)移以減少拷貝

下面是 左值引用右值引用 的對比圖

特征 左值引用 右值引用
語法 Type& lvalueRef = variable; Type&& rvalueRef = std::move(variable);
綁定對象 現(xiàn)有對象 臨時對象或可移動對象
典型用途 函數(shù)參數(shù)、返回類型 移動語義、完美轉(zhuǎn)發(fā)
示例 int x = 10; int& ref = x; int&& rref = 10;
可重新賦值
可為 nullptr 是(需謹(jǐn)慎使用)
引用折疊(C++11) Type&& && 折疊為 Type&&
生命周期延長(C++20) 否(延長臨時對象的生命周期) 是(綁定到臨時對象,如果綁定到右值則延長生命周期)

注意:表格提供了一個高層次的概述,實際上有更多的細(xì)節(jié)和差異,尤其是在C++的后續(xù)版本中引入的一些特性

1.4.右值引用的使用場景

右值 分為

  • 純右值(內(nèi)置類型)
  • 將亡值(自定義類型)

純右值 價值不大,但 將亡值 就不一樣了,直接轉(zhuǎn)移 將亡值 資源可以減少 拷貝次數(shù),所以 右值引用 的使用場景主要體現(xiàn)了 拷貝

下面是簡單模擬實現(xiàn)的 string,其中并未涉及 右值引用 相關(guān)知識

簡單模擬實現(xiàn)的 string

namespace Yohifo
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷貝構(gòu)造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷貝" << endl;
			string tmp(s._str);
			swap(tmp);
		}

		// 賦值重載
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷貝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		string operator+(char ch)
		{
			string tmp(*this);
			tmp += ch;
			return tmp;
		}

		const char* c_str() const
		{
			return _str;
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做標(biāo)識的\0
	};
}

為了更好的觀察是否發(fā)生了 深拷貝行為,在 拷貝構(gòu)造 函數(shù)中加入了對應(yīng)的打印語句,這里的參數(shù)為 const 左值引用

主函數(shù)中測試 左值、右值 兩種拷貝構(gòu)造

主函數(shù) main.cpp

int main()
{
	Yohifo::string str = "Hello World!";

	// str 為左值
	Yohifo::string s1 = str;

	// str+'\n' 為右值
	Yohifo::string s2 = str + '\n';

	return 0;
}

為什么要加 \n
防止編譯器直接將 拷貝構(gòu)造 優(yōu)化為 直接構(gòu)造

首先是測試 C++11 之前的結(jié)果(沒有 右值引用

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言
可以看到這里發(fā)生了 3深拷貝 行為,其中一次為 str 拷貝構(gòu)造,一次為 str + '\n' 拷貝構(gòu)造,還有一次是 operator+() 函數(shù)中的拷貝行為(無法避免)

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

現(xiàn)在足以證明,在沒有使用 右值引用 的情況下,即便是傳入 右值,觸發(fā)的也是 深拷貝,浪費了 右值 這個臨時資源

注意:如果此時只顯示了兩次深拷貝,那是因為 VS 的平臺工具集 v143 存在問題,會將 str+'\n' 這次拷貝構(gòu)造優(yōu)化掉,解決方法就是將平臺工具集改為 v142

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

接下來在 string 中重載一個 拷貝構(gòu)造 函數(shù),參數(shù)為 右值引用,此時稱為 移動構(gòu)造

移動構(gòu)造 string() — 位于 string

// 移動構(gòu)造
string(string&& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	cout << "string(string&& s) -- 移動構(gòu)造" << endl;
	swap(s);
}

編譯后再次運行,可以看到此時少了一次 深拷貝,多了一次 移動構(gòu)造

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

移動構(gòu)造 是由 str+'\n' 拷貝構(gòu)造時觸發(fā)的,又因為參數(shù)是 右值(臨時對象),所以這里的 string 對象只需與 “右值” 進(jìn)行 swap 就行了

可以通過調(diào)試證明 s2 的資源是從其他地方 “轉(zhuǎn)移” 過來的

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言


如今的編譯器都很智能,會自動進(jìn)行優(yōu)化以減少拷貝,比較典型的就是 構(gòu)造 + 拷貝構(gòu)造 優(yōu)化為直接構(gòu)造,那么對于 移動構(gòu)造 編譯器是否會做出優(yōu)化?

為了模擬優(yōu)化場景,這里簡單實現(xiàn)一個 to_string,目的是為了在函數(shù)結(jié)束后返回一個 臨時對象

整型轉(zhuǎn)為字符串 to_string() — 位于命名空間 Yohifo

string to_string(int val)
{
	bool flag = false;
	if (val < 0)
	{
		flag = true;
		val *= -1;
	}

	string ret;
	while (val)
	{
		int n = val % 10;
		ret += n + '0';
		val /= 10;
	}

	if(flag)
		ret += '-';

	std::reverse(ret.begin(), ret.end());

	return ret;
}

主函數(shù)中就負(fù)責(zé)調(diào)用 to_string() 獲得一個臨時對象,然后通過該臨時對象去構(gòu)造一個對象

int main()
{
	Yohifo::string str(Yohifo::to_string(100) + 'a');

	return 0;
}

首先來看看 C++11 之前(屏蔽 移動構(gòu)造

結(jié)果為 3 次深拷貝

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言
分析:第一次為 to_string() 函數(shù)執(zhí)行完后返回的臨時對象的拷貝,第二次為 operator+() 函數(shù)中生成的臨時對象(不可避免),第三次為 strto_string() + 'a' 形成的臨時對象的拷貝

實際拷貝次數(shù)不止 3 次,就拿 to_string() 函數(shù)來說,需要先將 ret 拷貝給 臨時對象,再將 臨時對象 拷貝給 調(diào)用者,編譯器在這里會優(yōu)化,優(yōu)化成一次拷貝構(gòu)造:ret 拷貝給調(diào)用者

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

這里的 深拷貝 是可以避免的,現(xiàn)在重新啟用 移動構(gòu)造 函數(shù),再看看結(jié)果

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

可以看到 3深拷貝 變成了 2移動構(gòu)造 + 1深拷貝(不可避免)

分析:第一次拷貝的對象是 臨時對象(右值),資源即將銷毀,觸發(fā) 移動構(gòu)造,將資源及時轉(zhuǎn)移;第三次拷貝也是如此,同樣可以通過 移動構(gòu)造 將臨時對象資源轉(zhuǎn)移

對于 to_string() 函數(shù)來說,也不應(yīng)該只發(fā)生一次 移動構(gòu)造,實際應(yīng)該先把 ret 拷貝給 臨時對象,再將 臨時對象 中的資源轉(zhuǎn)移;但編譯器判斷 ret 是一個局部變量,出了函數(shù)就銷毀了,于是就優(yōu)化成了 return move(ret); 函數(shù)返回時將 ret 中的資源通過 move 函數(shù)轉(zhuǎn)移

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

由此可以看出,編譯器會在 臨時對象 當(dāng)作中間人連續(xù)賦值的場景中,直接將 臨時對象 優(yōu)化掉,盡量減少拷貝,這才有了 to_string() 函數(shù)中最終看到的 一次拷貝構(gòu)造 / 一次移動構(gòu)造

言歸正傳,得益于 移動構(gòu)造,臨時對象 的資源得到了回收利用,傳值返回時不再需要經(jīng)過無意義且低效的 深拷貝

這里只是一個小小的 string,如果是 vectormap、unordered_map 等基于模板的復(fù)雜容器,移動構(gòu)造 帶來的效率提升是非常顯著的


關(guān)于 移動構(gòu)造 相關(guān)問題

Q1:能否將函數(shù)返回值設(shè)為 右值引用?

答案是 不行,不是說單純的 右值引用 解決了 無效深拷貝 問題,而是基于 右值引用 實現(xiàn)的 移動構(gòu)造 解決了問題,所以無論是 右值引用 還是 左值引用,在面對 傳值返回時,都不能作為函數(shù)返回值類型,返回局部對象引用會導(dǎo)致程序異常退出

并且在使用 右值引用 作為返回類型時,需要手動把 ret 這個左值 move,否則無法編譯(右值引用不能直接引用左值),即使編譯通過了,運行后也是有問題的

有問題的函數(shù) to_string()

string&& to_string(int val)
{
	bool flag = false;
	if (val < 0)
	{
		flag = true;
		val *= -1;
	}

	string ret;
	while (val)
	{
		int n = val % 10;
		ret += n + '0';
		val /= 10;
	}

	if(flag)
		ret += '-';

	std::reverse(ret.begin(), ret.end());

	return move(ret);
}

可以看到,不僅沒有觸發(fā) 移動構(gòu)造,還迫使程序異常終止(退出碼不為 0

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

Q2: 函數(shù)傳值返回,但在返回時能否手動 move 返回值?

答案是 可以的,前面說過,編譯器優(yōu)化后,會自動給返回值加上 move 以取出其中的資源,所以這里手動加上也沒問題,但沒必要

結(jié)果也是正常的
C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

Q3: 右值引用什么時候作為參數(shù)類型使用?

當(dāng)傳入的參數(shù)為 右值 時,推薦使用 右值引用 作為參數(shù)類型;如果既有傳入 左值 也有傳入 右值 的情況,可以重載一個 右值引用 參數(shù)版本,編譯器會匹配最合適的版本,確保資源不被浪費

常見的 右值引用 作為參數(shù)類型的有:拷貝構(gòu)造函數(shù)賦值重載函數(shù)(這兩個函數(shù)都是重載版本),傳值拷貝是比較低效的行為,有了這兩個函數(shù)后, 中其他函數(shù)可以放心傳值返回

力扣題目 「楊輝三角」中的函數(shù)返回值為 vector<vector<int>>,只要 vector 中實現(xiàn)了 移動構(gòu)造 函數(shù),就可以避免深拷貝,輕松返回結(jié)果

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

1.5.右值引用的意義

右值引用 是個好東西,它的核心功能在于再次利用 臨時資源,避免無意義且低效的拷貝行為

右值引用左值引用 各有各的適用場景:左值引用 是引用返回以提高效率(減少拷貝);右值引用 則是移動構(gòu)造提高效率(減少拷貝),兩者的角度不同

  • 左值引用:直接引用對象以減少拷貝
  • 右值引用:間接減少拷貝,將臨時資源等將亡值的資源通過 移動構(gòu)造 進(jìn)行轉(zhuǎn)移,減少拷貝

2.完美轉(zhuǎn)發(fā)

泛型編程C++ 中的核心功能之一,典型的讓程序員少走彎路,讓編譯器多干活,伴隨著 右值引用 的新概念加入,泛型編程 也需要隨之升級

2.1.模板中的萬能引用

泛型編程 的核心在于 模板根據(jù)參數(shù)類型推導(dǎo)函數(shù),當(dāng)我們分別傳入 左值引用右值引用 時,模板 是否能正確推導(dǎo)呢

下面這段代碼的含義是 分別傳入 左值const 左值、右值const 右值,并設(shè)計對應(yīng)參數(shù)的回調(diào)函數(shù),將參數(shù)傳給模板,看看模板是否能正確回調(diào)函數(shù)

void func(int& a)
{
	cout << "func(int& a) 左值引用" << endl;
}

void func(const int& a)
{
	cout << "func(const int& a) const 左值引用" << endl;
}

void func(int&& a)
{
	cout << "func(int&& a) 右值引用" << endl;
}

void func(const int&& a)
{
	cout << "func(const int&& a) const 右值引用" << endl;
}

template<class T>
void perfectForward(T&& val)
{
	// 調(diào)用函數(shù)
	func(val);
}

int main()
{
	int a = 10;
	const int b = 10;

	// 左值
	perfectForward(a);
	perfectForward(b);

	// 右值
	perfectForward(move(a));
	perfectForward(move(b));

	return 0;
}

注:move(const 左值) 可以獲取 const 右值

模板中涉及引用參數(shù)傳遞時,可以將函數(shù)參數(shù)類型寫為 T&&,因為模板具有自動推導(dǎo)的特性,當(dāng)傳入的參數(shù)為 左值 時,觸發(fā) 引用折疊 機(jī)制,實際參數(shù)類型會變?yōu)?T&;當(dāng)傳入的參數(shù)為 右值 時,正常使用 T&& 就行了
這一機(jī)制在模板中稱為 萬能引用(引用折疊),既能推導(dǎo) 左值引用,也能推導(dǎo) 右值引用

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

預(yù)期結(jié)果:先調(diào)用 左值引用、const 左值引用 版本的 func,再調(diào)用 右值引用const 右值引用 版本的 func

實際運行結(jié)果如下

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

調(diào)用的全是 左值引用 相關(guān)的 func,難道一向靠譜的模板推導(dǎo)出現(xiàn)問題了嗎?

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

當(dāng)然不是模板 是根據(jù)我們傳入的參數(shù)類型,來推導(dǎo)出相應(yīng)的函數(shù),如果說 模板 推導(dǎo)沒有問題,那問題就出在 回調(diào)函數(shù) 的參數(shù)上了,只有推導(dǎo)后,無論傳的 左值 還是 右值,編譯器都會把 val 變?yōu)?左值,這樣才能解釋為什么最終結(jié)果全部為 左值引用const 左值引用

編譯器這么做合理嗎?

非常合理,首先要明白 右值 是無法被取地址的,而 右值引用 是將 右值 中的資源轉(zhuǎn)存到一塊特定的空間中,這也就意味著 右值引用 后的值,必定是一個 左值(擁有空間,可取地址),只有為 左值 才可以對其進(jìn)行修改等操作

簡單來說就是 右值屬性轉(zhuǎn)早了

解決問題的核心在于 perfectForward 傳遞 val 參數(shù)時,如何保證它的 右值屬性 不丟失

2.2.傳參過程中保持右值屬性

要想在參數(shù)傳遞過程中保持其 右值屬性,就需要使用 forward 函數(shù),也就是 完美轉(zhuǎn)發(fā)

forward 是一個帶有參數(shù)模板的函數(shù),主要在傳參時使用: 如果參數(shù)原本是右值,但在右值引用后失去了右值屬性,使用 forward 函數(shù)可以恢復(fù)它的右值屬性

template<class T>
void perfectForward(T&& val)
{
	// 調(diào)用函數(shù)
	func(forward<T>(val));
}

再次運行程序,可以發(fā)現(xiàn)調(diào)用結(jié)果符合預(yù)期

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

注意:forward 是一個模板函數(shù),需要指定模板參數(shù)類型 T,確保能正確推導(dǎo)并傳遞

2.2.完美轉(zhuǎn)發(fā)實際應(yīng)用

完美轉(zhuǎn)發(fā) 在實際開發(fā)中會經(jīng)常用到,前面說過,在 C++11 之后,所有的類都可以新增一個 移動構(gòu)造 以規(guī)避無意義的低效拷貝行為,并且由于大部分類中會涉及 模板 的使用,保持右值屬性 就是一個必備的技巧,如果沒有 完美轉(zhuǎn)發(fā),那么 移動構(gòu)造 頂多也就減少了一次 深拷貝

接下來看看 完美轉(zhuǎn)發(fā) 如何應(yīng)用

首先準(zhǔn)備一個模擬實現(xiàn)的 list

#pragma once
#include<assert.h>

namespace Yohifo
{
	template<class T>
	struct list_node
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _data;

		list_node(const T& x = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _data(x)
		{}
	};

	template<class T, class Ref, class Ptr>
	struct __list_iterator
	{
		typedef list_node<T> node;
		typedef __list_iterator<T, Ref, Ptr> self;
		node* _node;

		__list_iterator(node* n)
			:_node(n)
		{}

		Ref operator*()
		{
			return _node->_data;
		}

		Ptr operator->()
		{
			return &_node->_data;
		}

		self& operator++()
		{
			_node = _node->_next;

			return *this;
		}

		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;

			return tmp;
		}

		self& operator--()
		{
			_node = _node->_prev;

			return *this;
		}

		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;

			return tmp;
		}

		bool operator!=(const self& s)
		{
			return _node != s._node;
		}

		bool operator==(const self& s)
		{
			return _node == s._node;
		}
	};

	template<class T>
	class list
	{
		typedef list_node<T> node;
	public:
		typedef __list_iterator<T, T&, T*> iterator;
		typedef __list_iterator<T, const T&, const T*> const_iterator;

		iterator begin()
		{
			return iterator(_head->_next);
		}

		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}

		iterator end()
		{
			return iterator(_head);
		}

		const_iterator end() const
		{
			return const_iterator(_head);
		}

		void empty_init()
		{
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;
		}

		list()
		{
			empty_init();
		}

		template <class Iterator>
		list(Iterator first, Iterator last)
		{
			empty_init();

			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}
		
		void swap(list<T>& tmp)
		{
			std::swap(_head, tmp._head);
		}

		list(const list<T>& lt)
		{
			empty_init();

			list<T> tmp(lt.begin(), lt.end());
			swap(tmp);
		}

		list<T>& operator=(list<T> lt)
		{
			swap(lt);
			return *this;
		}

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				//it = erase(it);
				erase(it++);
			}
		}

		void push_back(const T& x)
		{
			insert(end(), x);
		}

		void push_front(const T& x)
		{
			insert(begin(), x);
		}

		void pop_back()
		{
			erase(--end());
		}

		void pop_front()
		{
			erase(begin());
		}

		void insert(iterator pos, const T& x)
		{
			node* cur = pos._node;
			node* prev = cur->_prev;

			node* new_node = new node(x);

			prev->_next = new_node;
			new_node->_prev = prev;
			new_node->_next = cur;
			cur->_prev = new_node;
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());

			node* prev = pos._node->_prev;
			node* next = pos._node->_next;

			prev->_next = next;
			next->_prev = prev;
			delete pos._node;

			return iterator(next);
		}
	private:
		node* _head;
	};
}

因為在構(gòu)建鏈表節(jié)點時,是不需要 深拷貝 的,可以給 節(jié)點類 增加 移動構(gòu)造函數(shù)

新增鏈表節(jié)點的移動構(gòu)造 list_node — 位于 list_node

list_node(T&& x)
	:_next(nullptr)
	, _prev(nullptr)
	, _data(x)
{}

主函數(shù)中只需創(chuàng)建一個 list<string> 對象,,查看 移動構(gòu)造是否被正確調(diào)用

注意:這里的 list、string 都是模擬實現(xiàn)的

測試移動構(gòu)造是否生效

int main()
{
	Yohifo::list<Yohifo::string> l;
	l.push_back("Hello World!");

	return 0;
}

執(zhí)行結(jié)果為 兩次深拷貝

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

第一次深拷貝為構(gòu)造時觸發(fā)(默認(rèn)構(gòu)造傳的是 右值),第二次則是插入時觸發(fā)(插入的也是 右值

這里在 構(gòu)造 / 插入 時使用的可是 右值 啊,為什么 string 中的 移動構(gòu)造 函數(shù)沒有被正確調(diào)用呢?進(jìn)入調(diào)試模式,發(fā)現(xiàn)第一個問題:沒有給 list 提供右值引用版本的 push_back()

這里先提供一個 右值引用版本push_back(),并在參數(shù)傳遞時使用 完美轉(zhuǎn)發(fā),看看能不能解決問題

右值引用版的 push_back() — 位于 list

// 右值引用版
void push_back(T&& x)
{
	// 完美轉(zhuǎn)發(fā)
	insert(end(), std::forward<T>(x));
}

結(jié)果仍然是兩次 深拷貝

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

原因是因為 push_back() 并沒有干實事,它自己也在調(diào)用 insert(),而 insert() 還沒有提供 右值引用 版,這里先試著補(bǔ)上

// 右值引用版
void insert(iterator pos, T&& x)
{
	node* cur = pos._node;
	node* prev = cur->_prev;

	// 完美轉(zhuǎn)發(fā)
	node* new_node = new node(std::forward<T>(x));

	prev->_next = new_node;
	new_node->_prev = prev;
	new_node->_next = cur;
	cur->_prev = new_node;
}

已經(jīng)增加了兩次 完美轉(zhuǎn)發(fā) 了,結(jié)果仍是兩次 深拷貝

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

仔細(xì)觀察 insert() 的代碼可以發(fā)現(xiàn),在插入節(jié)點之前,需要先構(gòu)建一個 node 節(jié)點對象,構(gòu)建對象時已經(jīng)進(jìn)行了 完美轉(zhuǎn)發(fā),意味著當(dāng)前參數(shù)傳遞沒有問題,順著線索來到 node移動構(gòu)造 函數(shù)中

list_node(T&& x)
	:_next(nullptr)
	, _prev(nullptr)
	, _data(x)
{}

其中的 _data 也就是 string 對象,在構(gòu)造時,是直接傳遞了 x,并沒有對其進(jìn)行 完美轉(zhuǎn)發(fā),從而導(dǎo)致最終傳給 string 的是一個 左值,自然調(diào)用的就是 深拷貝 了,話不多說,再加上一次 完美轉(zhuǎn)發(fā)

list_node(T&& x)
	:_next(nullptr)
	, _prev(nullptr)
	, _data(std::forward<T>(x))
{}

再次運行程序,發(fā)現(xiàn)這次終于成功調(diào)用了 string移動構(gòu)造 函數(shù)

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言


要想讓我們之前模擬實現(xiàn)的 list 成功進(jìn)行 移動構(gòu)造,需要增加:一個移動構(gòu)造、兩個右值引用版本的函數(shù)、三次完美轉(zhuǎn)發(fā),并且整個 完美轉(zhuǎn)發(fā) 的過程是層層遞進(jìn)、環(huán)環(huán)相扣的,但凡其中有一層沒有進(jìn)行 完美轉(zhuǎn)發(fā),就會導(dǎo)致整個傳遞鏈路失效,無法觸發(fā) 移動構(gòu)造

所以對于這種涉及多次函數(shù)回調(diào)的類,需要確保 右值 傳遞的每一層都不會丟失 右值屬性,否則 移動構(gòu)造 就斷了


3.新增類功能

C++11 中新增了 右值引用 + 移動語義,應(yīng)用到類中就誕生了 移動構(gòu)造移動賦值 函數(shù),除此之外,還對類中參數(shù)可能為 右值 的函數(shù)重載了 右值引用 版本

3.1.移動構(gòu)造和移動賦值

之前類中有六個天選之子:構(gòu)造函數(shù)、析構(gòu)函數(shù)、拷貝構(gòu)造、賦值重載、取地址重載 和 const 取地址重載

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

有了 右值引用 + 移動語義 后,對 拷貝構(gòu)造賦值重載 進(jìn)行了 “升級”,增加了 移動構(gòu)造移動賦值 這兩個新函數(shù),至此,類中共有八個天選之子(編譯器會默認(rèn)生成)

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

天選之子 的意思就是 即使我們不寫,編譯器也會默認(rèn)生成(有條件)

之前六個 天選之子 的生成規(guī)則這里就不再闡述了,主要來說說 移動語義 相關(guān)的兩個函數(shù)

移動語義就是通過右值引用將資源轉(zhuǎn)移再利用

這兩個函數(shù)生成的條件比較苛刻:

  1. 如果自己沒有寫 移動構(gòu)造 ,并且沒有實現(xiàn) 析構(gòu)、拷貝構(gòu)造、賦值重載 中的任意一個,那么編譯器才會自動生成一個 移動構(gòu)造 函數(shù),移動構(gòu)造 函數(shù)對于內(nèi)置類型,會按字節(jié)拷貝,對于自定義類型,會去調(diào)用它的 移動構(gòu)造 函數(shù),如果沒有,就調(diào)用 拷貝構(gòu)造(目的:涉及深拷貝的類編譯器期望我們自己設(shè)計 移動構(gòu)造 函數(shù))
  2. 移動賦值 的生成邏輯與上面一致

編譯器為什么會這么要求?

如果我們實現(xiàn)了 析構(gòu)、拷貝構(gòu)造、賦值重載,就證明當(dāng)前的類中涉及到了 動態(tài)內(nèi)存管理,是需要自己進(jìn)行 深拷貝 的,編譯器無能為力,移動語義 也應(yīng)該根據(jù)自己的實際場景進(jìn)行設(shè)計,所以編譯器就沒有自動生成

如何自己實現(xiàn)這兩個 移動語義 相關(guān)函數(shù)?

得益于 右值引用,這個實現(xiàn)起來并不復(fù)雜,以 string 為例,移動構(gòu)造移動賦值 的實現(xiàn)如下

// 移動構(gòu)造
string(string&& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	swap(s);
}

// 移動賦值
string& operator=(string&& s)
{
	swap(s);
	return *this;
}

核心在于 與臨界資源(將亡值)交換資源

默認(rèn)生成的 移動構(gòu)造 或者 移動賦值 并非沒有用,就像 拷貝構(gòu)造 一樣,默認(rèn)生成的拷貝構(gòu)造會去調(diào)用該函數(shù)中涉及類的 拷貝構(gòu)造,也就是說,只要底層類沒問題,自動生成的函數(shù)也可以實現(xiàn) 深拷貝 / 移動構(gòu)造 / 移動賦值


如果非要使用編譯器默認(rèn)生成的呢?

在想讓編譯器生成的函數(shù)之后加上 default 關(guān)鍵字,如果類中涉及 動態(tài)內(nèi)存管理(比如這里的 string,是不推薦使用默認(rèn)生成函數(shù)的,因為會涉及到 深拷貝

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

并且由于 移動構(gòu)造 屬于 構(gòu)造 家族,移動賦值 屬于 賦值 家族,移動構(gòu)造 / 移動賦值 存在的前提是 拷貝構(gòu)造 / 賦值重載 也存在,如果都使用默認(rèn)的,自然就無法 深拷貝


STL 中的容器都增加了 移動構(gòu)造移動賦值

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

3.2.插入系列的重載版本

除了 構(gòu)造 / 賦值 時提高效率,插入 時也能提高效率,也就是通過 右值引用 重載實現(xiàn) 移動語義 版的 插入函數(shù)

比如之前實現(xiàn)的 list

// 右值引用版
void insert(iterator pos, T&& x)
{
	node* cur = pos._node;
	node* prev = cur->_prev;

	// 完美轉(zhuǎn)發(fā)
	node* new_node = new node(std::forward<T>(x));

	prev->_next = new_node;
	new_node->_prev = prev;
	new_node->_next = cur;
	cur->_prev = new_node;
}

注意:如果移動語義版的插入函數(shù)中涉及函數(shù)回調(diào)、構(gòu)造對象等,就需要使用 完美轉(zhuǎn)發(fā) 保持右值的屬性,確保能成功調(diào)用移動語義版本的函數(shù)


STL 中同樣更新了一波 移動語義 版的 插入函數(shù)

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言
說到底 移動語義 其實就是通過 右值引用 進(jìn)行資源轉(zhuǎn)移的行為

移動語義是否能延長臨時對象(將亡值)的生命周期?
不能,只是將其中的資源轉(zhuǎn)移了,但臨時對象(將亡值)本身仍然會被銷毀

const 引用延長生命周期問題
這是 C++11 之前對于右值的處理手段,在 push_back() 等插入函數(shù)值,常常會傳入一個臨時對象,此時就可以使用 const 引用作為參數(shù)類型來延長臨時對象的生命周期,伴隨 push_back() 棧幀銷毀而被銷毀


注意不要認(rèn)為 const 引用做返回值時能延長局部對象的生命周期,局部對象出了作用域就被銷毀了,而 const 引用此時指向被銷毀的對象,這是不合理的,是一種類似 “野指針” 的 “野引用” 行為

3.3.新增關(guān)鍵字

default 關(guān)鍵字

可以指定編譯器生成默認(rèn)的函數(shù),比如在下面這個類 Test 中,我們指定編譯器生成 構(gòu)造拷貝構(gòu)造

測試類 Test

class Test
{
public:
	// 構(gòu)造
	Test() = default;
	
	// 拷貝構(gòu)造
	Test(const Test&) = default;

private:
	Yohifo::string _str;
};

這里的是 string 是之前模擬實現(xiàn)的,方便查看調(diào)用的是 深拷貝 還是 移動構(gòu)造

分別傳入 左值右值 查看函數(shù)調(diào)用情況

int main()
{
	Test t1;

	Test t2(t1); // 傳入左值
	Test t3(move(t1)); // 傳入右值
	return 0;
}

可以看到當(dāng)前兩次都是 深拷貝,可以推斷出編譯器并沒有給 Test 自動生成 移動構(gòu)造,原因在于我們已經(jīng)指定生成了 拷貝構(gòu)造,編譯器認(rèn)為 Test 類中不具備自動生成 移動構(gòu)造 的條件

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

可以使用 defalut 指定編譯器自動生成 移動構(gòu)造

Test(Test&&) = default; // 指定生成移動構(gòu)造

再次運行程序,可以看到當(dāng)傳入 右值 進(jìn)行構(gòu)造時,調(diào)用的是 移動構(gòu)造

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

這里想強(qiáng)調(diào)的是 default 可以指定編譯器自動生成類中的默認(rèn)成員函數(shù)

能否使用 default 生成除默認(rèn)成員函數(shù)之外的其他成員函數(shù)?

答案是 不行,如果這都可以的話,編譯器都能自動寫代碼了,能自動生成默認(rèn)成員函數(shù),是因為這些函數(shù)的實現(xiàn)方式都是有模板的,編譯器可以直接套用

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言


delete 關(guān)鍵字

除了 default 關(guān)鍵字,C++11 還提供了 delete 關(guān)鍵字,用法和 default 一樣,不過 delete 是聲明該函數(shù)已被手動刪除,不可以使用,比如將 Test 中的 構(gòu)造 函數(shù)刪除,就無法構(gòu)造對象了

// 刪除構(gòu)造函數(shù)
Test() = delete;

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

什么情況下需要刪除函數(shù)?

比如在 單例模式 中,只允許創(chuàng)建一個對象,為了避免外部再次創(chuàng)建對象,需要將 構(gòu)造、拷貝構(gòu)造、移動構(gòu)造 等函數(shù)刪除;再比如 C++ 中的 IO 流類中,是不允許 IO 對象之間進(jìn)行拷貝的,因為每個 IO 對象中的緩沖區(qū)都不一樣,隨意拷貝會造成資源混亂,索性直接刪除了

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言


至于 finaloverride 已經(jīng)在 繼承和多態(tài) 相關(guān)章節(jié)介紹過了

  • final 修飾類,類不能被繼承
  • final 修飾成員函數(shù),子類繼承時,成員函數(shù)不能被重寫
  • override 修飾子類虛函數(shù),確保完成重寫

更多新增關(guān)鍵字詳見 C++11 官網(wǎng)

3.4.其他新功能

C++11 還修復(fù)之前 中的一個大坑:內(nèi)置類型不會初始化

這就導(dǎo)致如果你沒有在編寫 構(gòu)造 函數(shù)時對 內(nèi)置類型 進(jìn)行處理,會導(dǎo)致后續(xù)使用時出現(xiàn) 隨機(jī)值

比如下面這個類中就沒有對 內(nèi)置類型 進(jìn)行處理

class A
{
public:
	void Print()
	{
		cout << _a << endl;
	}

private:
	int _a;
};

int main()
{
	A a;
	a.Print();

	return 0;
}

打印結(jié)果為 隨機(jī)值

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

使用隨機(jī)值的危害?
如果將隨機(jī)值作為循環(huán)起始值,會導(dǎo)致循環(huán) “失控”

像這種大坑,估計是 C++ 獨有的,為了修復(fù)這個問題,C++11 中新增了一個小補(bǔ)丁:類成員變量初始化

就是在類成員定義時,允許給一個 缺省值,比如這樣

class A
{
	// ...
	
private:
	int _a = 0; // 此時給的是缺省值
};

此時輸出的結(jié)果就是可預(yù)期的

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

注意:這里給的是 缺省值,成員變量最終都是在 初始化列表中 進(jìn)行初始化的,定義時給缺失值,就可以在初始化列表中使用


C++11 中還新增了 委托構(gòu)造,就是允許在 初始化列表 中調(diào)用構(gòu)造函數(shù),這個語法作用并不是很大,并且不推薦使用,因為進(jìn)入初始化列表就已經(jīng)表示正在初始化了,再去調(diào)用其他構(gòu)造函數(shù)會顯得調(diào)用邏輯混亂

class A
{
public:
	A(int a = 0)
		:_a(a)
	{}

	// 拷貝構(gòu)造時,進(jìn)行委托構(gòu)造
	A(const A& a)
		:A()
	{
		// ...
	}

private:
	int _a;
};

注意:只有 構(gòu)造 相關(guān)函數(shù)才有 初始化列表,其他函數(shù)沒有這個東西,自然也就不能使用委托構(gòu)造


4.可變參數(shù)

C++11 引入了 可變參數(shù)模板可變參數(shù)包 的特性,允許定義和使用可接受任意數(shù)量參數(shù)的模板函數(shù),這對于編寫泛型代碼、容器等方面提供了更大的靈活性

4.1.可變參數(shù)列表

C 語言就已經(jīng)出現(xiàn)了 可變參數(shù),語法表示為 ...,C語言中的輸入輸出函數(shù)就用到了 可變參數(shù)列表

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

可變參數(shù) 的意思是你可以隨便傳入多個 參數(shù),函數(shù)都能進(jìn)行接收,C語言在使用 可變參數(shù)模板 時需要依賴 參數(shù)數(shù)量 + 參數(shù)類型 來進(jìn)行識別,簡單使用如下

int main()
{
	int a;
	double b;
	char c;
	scanf("%d %lf %c", &a, &b, &c);

	printf("輸入了: %d %lf %c\n", a, b, c);
	return 0;
}

雖然這里也支持接收任意數(shù)量的參數(shù),但還得提前確定這些參數(shù)的類型,使用起來比較麻煩

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

4.2.可變參數(shù)包

C++11 之前只能像 C語言 那樣使用固定參數(shù)的 可變參數(shù)列表,在 C++11 中進(jìn)行了重大改動,新增了 可變參數(shù)包,支持直接傳入任意數(shù)量、任意類型的參數(shù),不必像 C語言 那樣指定數(shù)量和類型,這個改動非常激進(jìn),導(dǎo)致整個 可變參數(shù) 語法變得十分抽象

把所有傳入的參數(shù),不論數(shù)量、類型,統(tǒng)統(tǒng)進(jìn)行打包,也就形成了 可變參數(shù)包

下面是使用 可變參數(shù)包 的實際例子(由于不知道會傳入什么類型的參數(shù),這里需要借助 可變參數(shù)模板

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

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言
C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

為了提高參數(shù)傳遞時的效率,可變參數(shù)包 的類型一般都會寫成 Args&&...
這在模板中稱為 萬能引用(引用折疊),既可以引用 左值,也可以引用 右值

可變參數(shù)模板 允許傳入 任意數(shù)量、任意類型 的參數(shù)

比如下面這幾種函數(shù)傳參都是可以的,由此可見 可變參數(shù)模板 的強(qiáng)大

int main()
{
	showList();
	showList(1, 2.2, 'c');
	showList("111111111111111");
	showList(vector<int>(), list<double>());
	return 0;
}

4.3.可變參數(shù)包的解析

可變參數(shù)模板 傳參簡單,可變參數(shù)包 解析就麻煩了,下面是一種不被編譯器支持的錯誤解析方式

template<class ...Args>
void showList(Args... args)
{
	// 錯誤的解析參數(shù)方式
	int n = sizeof...(args);

	for (int i = 0; i < n; i++)
	{
		// 獲取具體的可變參數(shù)
		args[i];
	}
}

注:使用 sizeof 計算可變參數(shù)包的大小時,需要在 sizeof 之后緊跟 ...,表示要計算的對象是可變參數(shù)包

這種解析方式很符合直覺,但編譯器并不支持,具體報錯信息為 必須在此上下文中擴(kuò)展參數(shù)包

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

“上下文” 是一個抽象的術(shù)語,用于描述代碼執(zhí)行時所處的特定環(huán)境,這個環(huán)境可能是與函數(shù)調(diào)用相關(guān)的,也可能是其他方面的,這里的 上下文 具體指 模板的實例化和展開時的環(huán)境和情境

模板 的實例化和展開可以借助 遞歸 來實現(xiàn)

// 遞歸推導(dǎo)時結(jié)束時調(diào)用的函數(shù)
void showList()
{}

template<class T, class ...Args>
void showList(const T& val, Args... args)
{
	cout << val << " ";
	showList(args...); // 遞歸解析
}

int main()
{
	showList(1, 2.2, 'c');

	return 0;
}

可變參數(shù)包 的參數(shù)被成功解析了

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

因為是 遞歸 解析的,所以需要一個遞歸出口,也就是 參數(shù)為 void 的重載函數(shù),推導(dǎo)邏輯如下

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

相關(guān)模板參數(shù)在編譯階段就已經(jīng)全部推導(dǎo)出來了,也就是說當(dāng)程序運行時,在當(dāng)前代碼中,會同時存在 4showList() 的重載函數(shù),可以通過 __FUNCTION__ 這個和宏以及 sizeof 驗證

template<class T, class ...Args>
void showList(const T& val, Args... args)
{
	cout << __FUNCTION__ << "(" << sizeof...(args) << ")" << endl;
	showList(args...); // 遞歸解析
}

可以看到 可變參數(shù)模板 中的函數(shù)共被調(diào)用了 3 次,再加上 showList() 無參版的調(diào)用,總共就是 4 個重載函數(shù)

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

main 函數(shù)第一次調(diào)用時,1 被賦給了 val,args 參數(shù)個數(shù)變成了兩個

除了這種 遞歸 解析參數(shù)包的方式外,還有一種奇特的解析方式 通過逗號表達(dá)式展開

具體實現(xiàn)如下

template<class T>
void Print(T val)
{
	// 獲取參數(shù)
	cout << val << " ";
}

template<class ...Args>
void showList(Args... args)
{
	int arr[] = { (Print(args), 0)... };
}

關(guān)鍵點在于 arr 數(shù)組創(chuàng)建時,會根據(jù) { } 中的參數(shù)進(jìn)行初始化,可以在此直接將 可變參數(shù)包展開,展開過程中就完成了 參數(shù) 的解析工作

為什么要寫出成 (Print(args), 0) 的形式?
這是一個逗號表達(dá)式,目的是讓整個式子最終返回 0,用于初始化 arr 數(shù)組

可以設(shè)置 Print() 的返回值來簡化代碼

template<class T>
int Print(T val)
{
	// 獲取參數(shù)
	cout << val << " ";

	return 0;
}

template<class ...Args>
void showList(Args... args)
{
	int arr[] = { Print(args)... };
}

編譯后代碼如下

template<class ...Args>
void showList(Args... args)
{
	int arr[] = { Print(1), Print(2.2), Print('c') };
}

這種參數(shù)包展開方式比較少用,簡單了解即可


可變參數(shù)包 的應(yīng)用場景在哪?

主要用于 線程回調(diào)函數(shù) 的參數(shù)傳遞,pthread 提供的線程創(chuàng)建接口 pthread_create 中只能給 線程回調(diào)函數(shù) 傳遞一個 指針變量C++11 中的 線程庫 借助 可變參數(shù)包 進(jìn)行了封裝設(shè)計,可以在創(chuàng)建 線程 時輕易傳遞多個參數(shù)

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

注:這里的 Fn 是可調(diào)用的函數(shù)對象

關(guān)于 C++11 線程庫 的更多知識將會放到下一篇文章中詳談

除此之外,可變參數(shù)包 還可以用于優(yōu)化插入相關(guān)的函數(shù)

4.4.emplace 系列函數(shù)

C++11 還升級了 STL 中的插入函數(shù)(非右值引用版),這些新增的函數(shù)依賴 可變參數(shù)包,稱為 emplace 系列

比如 listemplace_back()

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

empalce_back() 具備 push_back() 的所有功能,并且還在它的基礎(chǔ)上進(jìn)行了升級

如果只是單純插入 左值 或者 move(左值),這兩個函數(shù)沒有區(qū)別

int main()
{
	std::list<Yohifo::string> l;

	Yohifo::string str1 = "Hello";
	Yohifo::string str2 = "Hello";

	// 插入左值
	l.push_back(str1);
	l.emplace_back(str2);
	cout << endl;

	// 插入 move 出來的右值
	l.push_back(move(str1));
	l.emplace_back(move(str2));
	cout << endl;

	return 0;
}

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

但如果插入的是 純右值,兩個函數(shù)就有區(qū)別了

int main()
{
	//...
	
	// 插入純右值
	l.push_back("World");
	l.emplace_back("World");

	return 0;
}

插入純右值時,只發(fā)生了一次 移動構(gòu)造

C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

通過調(diào)試發(fā)現(xiàn),emplace_back() 在插入 純右值 "World" 時,甚至都沒有調(diào)用 移動構(gòu)造,而是直接走的 構(gòu)造函數(shù)

得益于 可變參數(shù)包,emplace 系列函數(shù)可以直接將 純右值 作為參數(shù)傳遞,傳遞途中不展開參數(shù)包,直到 構(gòu)造函數(shù) 才把參數(shù)包展開,充分發(fā)揮了 可變參數(shù)包 的優(yōu)勢(直接傳遞參數(shù))

因此可以得出結(jié)論:在插入純右值,并且構(gòu)造函數(shù)能正常接收時,emplace 系列函數(shù)可以直接構(gòu)造,省去了調(diào)用移動構(gòu)造函數(shù)時的開銷

為什么傳遞 "World" 可以直接構(gòu)造?
因為當(dāng)前模擬實現(xiàn)的 string 中,構(gòu)造函數(shù)參數(shù)就是 const char*,可以直接將參數(shù)包中的參數(shù)進(jìn)行傳遞

注意:插入 左值 或者 move(左值) 時,emplace 系列函數(shù)和普通函數(shù)沒區(qū)別


??總結(jié)

以上就是本次關(guān)于 C++11 中右值引用和移動語義的相關(guān)知識了,右值引用的引入解決了臨時資源過度消耗的問題,為類添加了移動語義函數(shù),同時也升級了插入函數(shù)以支持右值引用版本??勺儏?shù)包的引入簡化了多參數(shù)傳遞,尤其在 C++11 線程庫的使用中更為方便。新的 emplace 系列函數(shù)通過利用可變參數(shù)包,為類構(gòu)造函數(shù)提供了更靈活的調(diào)用方式,進(jìn)一步優(yōu)化了代碼的效率和可讀性。這些更新使得 C++11 更加強(qiáng)大、靈活


C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』,C++修行之路,c++,開發(fā)語言

文章來源地址http://www.zghlxwxcb.cn/news/detail-754364.html

相關(guān)文章推薦

C++ 進(jìn)階知識

C++11『基礎(chǔ)新特性』

C++ 哈希的應(yīng)用【布隆過濾器】

C++ 哈希的應(yīng)用【位圖】

C++【哈希表的完善及封裝】

C++【哈希表的模擬實現(xiàn)】

C++【初識哈希】

C++【一棵紅黑樹封裝 set 和 map】

到了這里,關(guān)于C++11『右值引用 ‖ 完美轉(zhuǎn)發(fā) ‖ 新增類功能 ‖ 可變參數(shù)模板』的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進(jìn)行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費用

相關(guān)文章

  • 從C語言到C++_33(C++11_上)initializer_list+右值引用+完美轉(zhuǎn)發(fā)+移動構(gòu)造/賦值

    從C語言到C++_33(C++11_上)initializer_list+右值引用+完美轉(zhuǎn)發(fā)+移動構(gòu)造/賦值

    目錄 1.?列表初始化initializer_list 2. 前面提到的一些知識點 2.1 小語法 2.2 STL中的一些變化 3. 右值和右值引用 3.1?右值和右值引用概念 3.2?右值引用類型的左值屬性 3.3?左值引用與右值引用比較 3.4?右值引用的使用場景 3.4.1 左值引用的功能和短板 3.4.2?移動構(gòu)造 3.4.3?移動賦值

    2024年02月12日
    瀏覽(34)
  • c++右值引用、移動語義、完美轉(zhuǎn)發(fā)

    左值:一般指的是在內(nèi)存中有對應(yīng)的存儲單元的值,最常見的就是程序中創(chuàng)建的變量 右值:和左值相反,一般指的是沒有對應(yīng)存儲單元的值(寄存器中的立即數(shù),中間結(jié)果等),例如一個常量,或者表達(dá)式計算的臨時變量 左值引用:C++中采用 對變量進(jìn)行引用,這種常規(guī)的引

    2024年02月05日
    瀏覽(18)
  • 【重學(xué)C++】05 | 說透右值引用、移動語義、完美轉(zhuǎn)發(fā)(下)

    【重學(xué)C++】05 | 說透右值引用、移動語義、完美轉(zhuǎn)發(fā)(下) 大家好,我是只講技術(shù)干貨的會玩code,今天是【重學(xué)C++】的第五講,在第四講《【重學(xué)C++】04 | 說透右值引用、移動語義、完美轉(zhuǎn)發(fā)(上)》中,我們解釋了右值和右值引用的相關(guān)概念,并介紹了C++的移動語義以及如

    2024年02月06日
    瀏覽(16)
  • 【重學(xué)C++】04 | 說透C++右值引用、移動語義、完美轉(zhuǎn)發(fā)(上)

    【重學(xué)C++】04 | 說透C++右值引用、移動語義、完美轉(zhuǎn)發(fā)(上) 大家好,我是只講技術(shù)干貨的會玩code,今天是【重學(xué)C++】的第四講,在前面《03 | 手?jǐn)]C++智能指針實戰(zhàn)教程》中,我們或多或少接觸了右值引用和移動的一些用法。 右值引用是 C++11 標(biāo)準(zhǔn)中一個很重要的特性。第一

    2024年02月06日
    瀏覽(22)
  • C++右值引用(左值表達(dá)式、右值表達(dá)式)(移動語義、完美轉(zhuǎn)發(fā)(右值引用+std::forward))(有問題懸而未決)

    C++右值引用(左值表達(dá)式、右值表達(dá)式)(移動語義、完美轉(zhuǎn)發(fā)(右值引用+std::forward))(有問題懸而未決)

    在 C++ 中,表達(dá)式可以分為左值表達(dá)式和右值表達(dá)式。左值表達(dá)式指的是可以出現(xiàn)在賦值語句左邊的表達(dá)式,例如變量、數(shù)組元素、結(jié)構(gòu)體成員等;右值表達(dá)式指的是不能出現(xiàn)在賦值語句左邊的表達(dá)式,例如常量、臨時對象、函數(shù)返回值等。 右值是指將要被銷毀的臨時對象或

    2024年02月04日
    瀏覽(22)
  • C++左值右值完美轉(zhuǎn)發(fā)轉(zhuǎn)移

    英文含義: 左值(Lvalue) : Locator value ,意味著它指向一個具體的內(nèi)存位置。 右值(Rvalue) : Read value ,指的是可以讀取的數(shù)據(jù),但不一定指向一個固定的內(nèi)存位置。 定義 左值 :指的是一個持久的內(nèi)存地址。左值可以出現(xiàn)在賦值操作的左側(cè)或右側(cè)。例如,變量、數(shù)組的元

    2024年03月10日
    瀏覽(35)
  • 【C++11】移動賦值 | 新的類功能 | 可變參數(shù)模板

    【C++11】移動賦值 | 新的類功能 | 可變參數(shù)模板

    C++11中,string中的operator= 包含 參數(shù)為右值的版本 C++98中 沒有移動賦值和移動構(gòu)造 , 只有參數(shù)為左值 的賦值重載(operator=)和拷貝構(gòu)造 本來只有兩次深拷貝,但是由于調(diào)用拷貝賦值時,內(nèi)部又進(jìn)行一次拷貝構(gòu)造,所以導(dǎo)致最終進(jìn)行三次深拷貝 這里編譯器是不能優(yōu)化的, 因為優(yōu)

    2024年02月08日
    瀏覽(26)
  • 【C++11】左值引用和右值引用

    【C++11】左值引用和右值引用

    需要云服務(wù)器等云產(chǎn)品來學(xué)習(xí)Linux的同學(xué)可以移步/--騰訊云--/--阿里云--/--華為云--/官網(wǎng),輕量型云服務(wù)器低至112元/年,新用戶首次下單享超低折扣。 ??? 目錄 一、新的類功能 1、新的默認(rèn)成員函數(shù) 2、類成員變量初始化 3、強(qiáng)制生成默認(rèn)函數(shù)的default 4、禁止生成默認(rèn)函

    2023年04月17日
    瀏覽(24)
  • 【送書】【C++11】左值引用和右值引用

    【送書】【C++11】左值引用和右值引用

    需要云服務(wù)器等云產(chǎn)品來學(xué)習(xí)Linux的同學(xué)可以移步/--騰訊云--/--阿里云--/--華為云--/官網(wǎng),輕量型云服務(wù)器低至112元/年,新用戶首次下單享超低折扣。 ??? 目錄 一、新的類功能 1、新的默認(rèn)成員函數(shù) 2、類成員變量初始化 3、強(qiáng)制生成默認(rèn)函數(shù)的default 4、禁止生成默認(rèn)函

    2023年04月09日
    瀏覽(18)
  • 【C++】C++11右值引用

    【C++】C++11右值引用

    ?? 樊梓慕: 個人主頁 ??? 個人專欄: 《C語言》 《數(shù)據(jù)結(jié)構(gòu)》 《藍(lán)橋杯試題》 《LeetCode刷題筆記》 《實訓(xùn)項目》 《C++》 《Linux》 《算法》 ?? 每一個不曾起舞的日子,都是對生命的辜負(fù) 目錄 前言 1.什么是左值什么是右值 左值 右值 2.什么是左值引用什么是右值引用 左

    2024年04月22日
    瀏覽(29)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包