目錄
1、右值引用和移動語義
1.1 左值和右值
1.2 左值引用和右值引用
1.3 引用延長生命周期
1.4 左值和右值的參數匹配
1.5 右值引用和移動語義的使用場景
1.5.1?左值引用主要使用場景
1.5.2 移動構造和移動賦值
1.5.3 右值引用和移動語義解決傳值返回問題
1.5.4 右值引用和移動語義在傳參中的提效
總結一下:
1.6 類型分類(了解)
1.7 引用折疊
1.7.1 語義折疊的概念
1.7.2 引用折疊的應用
1.8 完美轉發
2、可變參數模板
2.1 基本語法及原理
2.2 包擴展
2.2.1 直接擴展
2.2.2 遞歸擴展
2.2.3?函數調用式包擴展
2.3 emplace系列接口
3、類的新功能
3.1?默認的移動構造和移動賦值
3.2?成員變量聲明時給缺省值
3.3 default和delete
3.4 final和override
1、右值引用和移動語義
C++11之后中新增了的右值引用語法特性,C++11之前學習的引用就叫做左值引用。無論左值引用還是右值引用,都是給對象取別名。?
1.1 左值和右值
左值是一個表示數據的表達式(如變量名或解引用的指針等),一般是有持久狀態,存儲在內存中,可修改,我們可以獲取它的地址,左值可以出現在 " = " 的左邊或右邊。定義時const修飾后的左值,不能修改,但是可以取它的地址。
右值也是一個表示數據的表達式(如字面值常量或臨時對象等),一般沒有名稱、沒有內存地址,不可修改,右值不能取地址,右值只能出現在 " = " 的右邊。
左值(lvalue)傳統解釋是 left value,C++11之后,更準確地解釋為 locator value (定位值,可以取地址)
右值(rvalue)傳統解釋是 right value,C++11之后,更準確地解釋為 read?value (可讀值,不可取地址)
#include<iostream>
using namespace std;
int main()
{// 左值:可以取地址// 以下的 p、b、c、*p、s、s[0] 就是常見的左值int* p = new int(0);int b = 1;const int c = b;*p = 10;string s("111111");s[0] = 'x';cout << &c << endl;cout << (void*)&s[0] << endl; // cout打印char*是打印字符串,轉成void*才能打印地址// 右值:不能取地址// 以下幾個 10、x + y、fmin(x, y)、string("11111") 都是常見的右值double x = 1.1, y = 2.2;10;x + y;fmin(x, y);string("11111");// cout << &10 << endl;// cout << &(x+y) << endl;// cout << &(fmin(x, y)) << endl;// cout << &string("11111") << endl;return 0;
}
1.2 左值引用和右值引用
Type& r1 = x; 就是左值引用,給左值取別名;
Type&& rr1 = y; 就是右值引用,給右值取別名。
左值引用不能直接引用右值,但是const左值引用可以引用右值。
右值引用不能直接引用左值,但是右值引用可以引用move(左值)。
template <class _Ty>
remove_reference_t<_Ty>&& move(_Ty&& _Arg)
{// forward _Arg as movablereturn static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
move是庫里面的一個函數模板,本質內部是進行強制類型轉換(左值->右值),標記為可竊取資源的對象,也可以move(右值),還是右值(增強可讀性),當然還涉及一些引用折疊的知識,這個后面會細講。
注意:變量表達式都是左值屬性,那么,左值引用和右值引用本身是左值,可以被修改,如:上面的r1是左值引用,rr1是右值引用,但本身都是左值,可以被修改,那么右值(一般不可修改)就可以通過右值引用進行修改,就可以達到竊取資源的目的。
從語法層面看,左值引用和右值引用都是取別名,不開空間。從匯編底層的角度看下面代碼中r1和rr1的匯編層實現,底層都是用指針實現的,所以左值->右值。底層匯編等實現和上層語法表達的意義有時是背離的,所以不要混到一起理解,互相佐證反而會陷入迷途。
#include<iostream>
using namespace std;int main()
{// 左值:可以取地址// 以下的p、b、c、*p、s、s[0]就是常見的左值int* p = new int(0);int b = 1;const int c = b;*p = 10;string s("111111");s[0] = 'x';double x = 1.1, y = 2.2;// 左值引用給左值取別名int& r1 = b;int*& r2 = p;int& r3 = *p;string& r4 = s;char& r5 = s[0];// 右值引用給右值取別名int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);string&& rr4 = string("11111");// 左值引用不能直接引用右值,但是const左值引用可以引用右值const int& rx1 = 10;const double& rx2 = x + y;const double& rx3 = fmin(x, y);const string& rx4 = string("11111");// 右值引用不能直接引用左值,但是右值引用可以引用move(左值)int&& rrx1 = move(b);int*&& rrx2 = move(p);int&& rrx3 = move(*p);string&& rrx4 = move(s);string&& rrx5 = (string&&)s; // 強制類型轉化// b、r1、rr1都是變量表達式,都是左值cout << &b << endl;cout << &r1 << endl;cout << &rr1 << endl;int& r6 = r1;// 這里要注意的是,rr1的屬性是左值,所以不能再被右值引用綁定,除非move一下// int&& rrx6 = rr1;int&& rrx6 = move(rr1);return 0;
}
1.3 引用延長生命周期
注意:引用延長生命周期,只能延長在當前作用域的生命周期。
右值引用可用于延長臨時對象的生命周期,但右值引用本身是左值,可以被修改。
const的左值引用也能延長臨時對象的生命周期,但這些對象無法被修改。
#include <iostream>
#include <string>int main()
{std::string s1 = "Test";const std::string& r2 = s1 + s1; // OK:const左值引用可延長臨時對象生命周期// r2 += "Test"; // 錯誤:不能通過const引用修改std::string&& r3 = s1 + s1; // OK:右值引用延長臨時對象生存期r3 += "Test"; // OK:可通過非const右值引用修改std::cout << r3 << '\n';return 0;
}
1.4 左值和右值的參數匹配
C++98中,我們實現一個const左值引用作為形參的函數,那么實參傳遞左值和右值都可以匹配。
C++11以后,分別重載,左值引用、const左值引用、右值引用作為形參的 f 函數,會調用最匹配的。
#include <iostream>
using namespace std;void f(int& x) {cout << "左值引用重載 f(" << x << ")\n";
}void f(const int& x) {cout << "const左值引用重載 f(" << x << ")\n";
}void f(int&& x) {cout << "右值引用重載 f(" << x << ")\n";
}int main() {int i = 1;const int ci = 2;f(i); // 調用 f(int&)f(ci); // 調用 f(const int&)f(3); // 調用 f(int&&),若無此重載則調用 f(const int&)f(std::move(i)); // 調用 f(int&&)// 右值引用變量在表達式中是左值int&& x = 1;f(x); // 調用 f(int&)f(std::move(x)); // 調用 f(int&&)return 0;
}
1.5 右值引用和移動語義的使用場景
1.5.1?左值引用主要使用場景
左值引用主要使用場景是在函數中左值引用傳參和左值引用傳返回值時減少拷貝,同時還可以修改實參和修改返回對象的價值。
但是有些場景不能使用傳左值引用返回,如 addStrings 和 generate 函數,C++98 中的解決方案只能是被迫使用輸出型參數解決。那么 C++11 以后這里可以使用右值引用做返回值解決嗎?顯然是不能的,因為這里的本質是返回對象是一個局部對象,函數結束這個對象就析構銷毀了,右值引用返回,只能延長對象在當前函數棧幀的生命周期,但函數棧幀已經銷毀了,對象會析構,無力回天了。
class Solution {
public:// 傳值返回需要拷貝string addStrings(string num1, string num2) {string str;int end1 = num1.size() - 1;int end2 = num2.size() - 1;// 進位int next = 0;while (end1 >= 0 || end2 >= 0) {int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;int ret = val1 + val2 + next;next = ret / 10;ret = ret % 10;str += ('0' + ret);}if (next == 1) {str += '1';}reverse(str.begin(), str.end());return str;}// 這里的傳值返回拷貝代價就太大了vector<vector<int>> generate(int numRows) {vector<vector<int>> vv(numRows);for (int i = 0; i < numRows; ++i){vv[i].resize(i + 1, 1);}for (int i = 2; i < numRows; ++i){for (int j = 1; j < i; ++j){vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];}}return vv;}
};
1.5.2 移動構造和移動賦值
移動構造是一種構造函數,類似拷貝構造函數,
移動構造函數要求第一個參數是該類類型對象的右值引用,后面只能加缺省參數。
移動賦值是一個賦值運算符的重載,類似拷貝賦值函數,
移動賦值函數要求第一個參數是該類類型對象的右值引用。
對于像 string/vector 這樣的深拷貝的類或者包含深拷貝的成員變量的類,移動構造和移動賦值才有意義,因為移動構造和移動賦值的第一個參數都是右值引用的類型,他的本質是要“竊取”引用的右值對象的資源(右值對象一般是臨時對象(返回的局部對象也認為是臨時對象),直接swap臨時對象的資源,不走深拷貝),從提高效率。
注意:
1. move(左值)是讓左值->右值,本身沒有竊取資源,是移動構造和移動賦值竊取(swap)資源。
2. 對于內置類型,只需賦值就行,就算是移動,竊取的本質也是賦值,所以不需要移動語義。
3. 個人疑惑,移動構造為什么不叫移動拷貝構造,不是用右值對象來構造的嗎?因為拷貝,有不改變原對象的意思,避免混淆。
下面的 Lzc::string 樣例實現了移動構造和移動賦值,我們需要結合場景理解。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>
#include <string.h>
#include <algorithm>
using namespace std;namespace Lzc
{class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin() { return _str; }iterator end() { return _str + _size; }const_iterator begin() const { return _str; }const_iterator end() const { 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){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}string(const string& s){cout << "string(const string& s) -- 拷貝構造" << endl;reserve(s._capacity);for (auto ch : s){push_back(ch);}}// 移動構造string(string&& s){cout << "string(string&& s) -- 移動構造" << endl;swap(s);}string& operator=(const string& s){cout << "string& operator=(const string& s) -- 拷貝賦值" << endl;if (this != &s){_str[0] = '\0';_size = 0;reserve(s._capacity);for (auto ch : s){push_back(ch);}}return *this;}// 移動賦值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移動賦值" << endl;swap(s);return *this;}~string(){cout << "~string() -- 析構" << endl;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];if (_str){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;}const char* c_str() const { return _str; }size_t size() const { return _size; }private:char* _str = new char('\0');size_t _size = 0;size_t _capacity = 0;};
}int main()
{// 構造Lzc::string s1("xxxxx");// 拷貝構造Lzc::string s2 = s1;// 構造+移動構造,優化后直接構造Lzc::string s3 = Lzc::string("yyyyy");// 移動構造Lzc::string s4 = move(s1);cout << "******************************" << endl;return 0;
}
1.5.3 右值引用和移動語義解決傳值返回問題
namespace Lzc {string addStrings(string num1, string num2) {string str;int end1 = num1.size() - 1, end2 = num2.size() - 1;int next = 0;while (end1 >= 0 || end2 >= 0) {int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;int ret = val1 + val2 + next;next = ret / 10;ret = ret % 10;str += ('0' + ret);}if (next == 1)str += '1';reverse(str.begin(), str.end());cout << "******************************" << endl;return str;}
}int main() {Lzc::string ret;ret = Lzc::addStrings("11111", "2222");cout << ret.c_str() << endl;return 0;
}
g++ -std=c++11 test.cpp -fno-elide-constructors -o test && ./test
使用C++11標準,去掉編譯器優化,編譯為test,并執行。
1.5.4 右值引用和移動語義在傳參中的提效
查看STL文檔我們發現,C++11以后容器的push系列和insert系列的接口都增加了右值引用版本。
當實參是一個左值時,左值引用,容器內部繼續調用拷貝構造進行拷貝,將拷貝的對象放到容器空間中。
當實參是一個右值時,右值引用,容器內部則調用移動構造(由于右值引用本身是左值,會走拷貝構造,需move,轉成右值,走移動構造),將竊取了臨時對象的資源的對象放到容器空間中。
template<class T>struct list_node{T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T& data = T()):_data(data) // 拷貝構造,_next(nullptr),_prev(nullptr){}list_node(T&& data):_data(move(data))// 移動構造, _next(nullptr), _prev(nullptr){}};
其實這里還有一個emplace系列的接口,但是這個涉及可變參數模板,我們需要把可變參數模板講解以后再講解emplace系列的接口。
總結一下:
編譯器優化,不是C++的標準,取決于編譯器。
右值引用+移動語義 與 編譯器優化,差別不大,有時略勝一籌,只需支持C++11的右值引用和移動語義,不依賴編譯器。
但是,右值引用+移動語義 與 編譯器優化 = 完美。
多嘴一句:因為之前的C++委員會有點"擺爛",沒有出右值引用和移動語義,所以編譯器自己優化,所以現在理解有點難受。
1.6 類型分類(了解)
中文:值類別 - cppreference.com
英文:Value categories - cppreference.com
一般看,左值,右值。
-
左值(lvalue)?是指具有持久性、有名字的表達式,可以取地址,通常代表一個對象的內存位置。
-
純右值(prvalue)?是臨時對象,通常是計算過程中產生的中間結果,沒有名字,不能取地址。
-
將亡值(xvalue)?是即將被move的對象,通常是“可以被竊取資源”的右值。
?
1.7 引用折疊
1.7.1 語義折疊的概念
1. C++中不能直接定義引用的引用,如 int& && r = i; 這樣寫會直接報錯。
2. 模板或typedef中的類型操作可以構成引用的引用。 這時C++11給出了一個引用折疊的規則:右值引用的右值引用折疊成右值引用,所有其他組合均折疊成左值引用。?
#include <iostream>// 由于引用折疊限定,f1 實例化以后總是一個左值引用
template<class T>
void f1(T& x) {}// 由于引用折疊限定,f2 實例化后可以是左值引用,也可以是右值引用
// 也稱: 萬能引用
template<class T>
void f2(T&& x) {}int main() {typedef int& lref;typedef int&& rref;int n = 0;// 引用折疊示例lref& r1 = n; // r1 的類型是 int&lref&& r2 = n; // r2 的類型是 int&rref& r3 = n; // r3 的類型是 int&rref&& r4 = 1; // r4 的類型是 int&&// f1 函數模板實例化與調用情況// 沒有折疊 -> 實例化為 void f1(int& x)f1<int>(n);// f1<int>(0); // 報錯,不能將右值綁定到左值引用// 折疊 -> 實例化為 void f1(int& x)f1<int&>(n);// f1<int&>(0); // 報錯,不能將右值綁定到左值引用// 折疊 -> 實例化為 void f1(int& x)f1<int&&>(n);// f1<int&&>(0); // 報錯,不能將右值綁定到左值引用// 折疊 -> 實例化為 void f1(const int& x)f1<const int&>(n);f1<const int&>(0);// 折疊 -> 實例化為 void f1(const int& x)f1<const int&&>(n);f1<const int&&>(0);// f2 函數模板實例化與調用情況// 沒有折疊 -> 實例化為 void f2(int&& x)// f2<int>(n); // 報錯,不能將左值綁定到右值引用f2<int>(0);// 折疊 -> 實例化為 void f2(int& x)f2<int&>(n);// f2<int&>(0); // 報錯,不能將右值綁定到左值引用// 折疊 -> 實例化為 void f2(int&& x)// f2<int&&>(n); // 報錯,不能將左值綁定到右值引用f2<int&&>(0);return 0;
}
個人疑惑:
const int&& x,x是右值引用,右值一般不可修改,因為x本身是左值,所以可以修改,如果還加const,那就是右值不可修改,這用說嗎?而且只可以接受右值。
我const int& x,也是不可修改,還可以接受左值和右值。
但是一般使用函數模板,不顯示實例化,
那么萬能引用函數模板,的推導過程是:
傳右值,T就是右值的類型,T&&就是右值引用,
傳左值,T就是左值類型的左值引用,T&&就是左值引用。?
#include <iostream>
#include <utility> // movetemplate<class T>
void Function(T&& t) {int a = 0;T x = a;// x++;std::cout << &a << std::endl;std::cout << &x << std::endl << std::endl;
}int main() {// 10 是右值,推導出 T 為 int,模板實例化為 void Function(int&& t)Function(10);int a;// a 是左值,推導出 T 為 int&,引用折疊,模板實例化為 void Function(int& t)Function(a);// std::move(a) 是右值,推導出 T 為 int,模板實例化為 void Function(int&& t)Function(std::move(a));const int b = 8;// b 是 const 左值,推導出 T 為 const int&,引用折疊,模板實例化為 void Function(const int& t)// Function 內部會編譯報錯,因為 x 不能 ++Function(b);// std::move(b) 是 const 右值,推導出 T 為 const int,模板實例化為 void Function(const int&& t)// 所以 Function 內部會編譯報錯,x 不能 ++Function(std::move(b));return 0;
}
1.7.2 引用折疊的應用
左值引用和右值引用的函數,只有參數部分不同,函數體基本相同,高度相似->模板。
例:
template<class T> list{ };中的push_back,此時T&&不是萬能引用,因為list模板實例化了,T就已經確定了。要再加一層模板,才能構成萬能引用。
void push_back(const T& x){insert(end(), x);}void push_back(T&& x){insert(end(), move(x)); // x本身是左值}
那么list模板里面再來個函數模板,這個時候萬能引用就體現出價值了,
傳左值就實例化左值引用版本,傳右值就實例化右值引用版本。
但是有一個問題,不能直接move(x),因為可能是左值引用版本,若move(x),原對象的資源可能被竊取,改變了原對象。
-
左值 → 會調用拷貝語義(保留原對象)。
-
右值 → 會調用移動語義(允許“竊取”資源)。
// 萬能引用template<class X>void push_back(X&& x){insert(end(), x);}
這個時候就需要完美轉發了,右值引用(本身是左值)返回右值引用,左值引用返回左值引用,然后再傳。?
其實,這個邏輯沒錯,但是例子有點小瑕疵,如果是list<pair<int,int>>,沒有萬能引用,一開始就確定了T是pair<int,int>,可以push_back({1,2}),走類型轉換,但是如果寫成萬能引用,就不能push_back({1,2}),因為在傳參時,確定類型,不知道{1,2}是什么類型。
1.8 完美轉發
// 左值版本(T 是具體類型,如 int&)
template<typename T>
constexpr T&& forward(typename std::remove_reference<T>::type& t) noexcept {return static_cast<T&&>(t);
}// 右值版本(T 是具體類型,如 int&&)
template<typename T>
constexpr T&& forward(typename std::remove_reference<T>::type&& t) noexcept {static_assert(!std::is_lvalue_reference<T>::value, "Cannot forward rvalue as lvalue");return static_cast<T&&>(t);
}
std::remove_reference_t<T>&
?和?typename std::remove_reference<T>::type&
?在功能上是完全等價的?
形式 | 說明 | 引入標準 |
---|---|---|
typename std::remove_reference<T>::type& | 傳統的 traits 用法,需要?typename ?關鍵字 | C++98/03 |
std::remove_reference_t<T>& | C++14 引入的簡化寫法,_t ?后綴表示直接取類型 | C++14 |
remove_reference_t<T>:T變成非引用類型(去掉了傳過來的T中的&)。
static_cast<T&&>(x):T&&會引用折疊,x強制轉成T&&類型。
當?T
?是非引用類型時(即原始參數是右值),選擇過程如下:
情況1:傳入?右值(如?std::forward<int>(10)
)
-
模板參數?
T
?被推導為?int
(非引用類型) -
匹配過程:
-
左值版本參數:
remove_reference_t<int>&
?→?int&
? 無法綁定到右值?10
-
右值版本參數:
remove_reference_t<int>&&
?→?int&&
? 精確匹配右值
-
-
選擇右值版本
? static_cast<T&&>
?→ 生成右值引用類型的表達式
? ? ?編譯器魔法:這個表達式會被特殊標記為"可以匹配右值引用參數"。
情況2:傳入?左值(如?int x; std::forward<int&>(x)
)
-
模板參數?
T
?被推導為?int&
(左值引用) -
匹配過程:
-
左值版本參數:
remove_reference_t<int&>&
?→?int&
? 精確匹配左值 -
右值版本參數:
remove_reference_t<int&>&&
?→?int&&
? 無法綁定左值
-
-
選擇左值版本
static_cast<T&&>
?→ 生成左值引用類型的表達式
所以:
// 萬能引用template<class X>void push_back(X&& x){insert(end(), forward<T>(x));}
注意:萬能引用進行傳參時,通常需要完美轉發。
2、可變參數模板
2.1 基本語法及原理
C++11 支持可變參數模板,也就是說支持可變數量參數的函數模板和類模板,可變數目的參數被稱為參數包,存在兩種參數包:模板參數包,表示零或多個模板參數;函數參數包:表示零或多個函數參數。
template <class ...Args> void Func (Args... args) {}
template <class ...Args> void Func (Args&... args) {}
template <class ...Args> void Func (Args&&... args) {}
我們用省略號表示一個模板參數或函數參數的一個包,
在模板參數列表中,class... 或typename... 指出接下來的參數表示零或多個類型;
在函數參數列表中,類型名... 指出接下來的參數表示零或多個參數;
函數參數包可以用左值引用或右值引用表示,跟前面普通模板一樣,每個參數實例化時遵循引用折疊規則。
可變參數模板的原理跟普通模板類似,本質還是去實例化對應類型和個數的多個函數。
這里我們可以使用sizeof...運算符去計算參數包中參數的個數。
template <class ...Args>
void Print(Args&&... args) {cout << sizeof...(args) << endl;
}int main() {double x = 2.2;Print(); // 包里有0個參數Print(1); // 包里有1個參數Print(1, string("xxxxx")); // 包里有2個參數Print(1.1, string("xxxxx"), x); // 包里有3個參數return 0;
}// 原理1:編譯本質這里會結合引用折疊規則實例化出以下四個函數
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);// 原理2:更本質去看沒有可變參數模板,我們實現出這樣的多個函數模板才能支持
// 這里的功能,有了可變參數模板,我們進一步被解放,他是類型泛化基礎
// 上疊加數量變化,讓我們泛型編程更靈活。
template <class T1>
void Print(T1&& arg1);template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);
// ...
2.2 包擴展
對于一個參數包,我們除了能計算它的參數個數,我們能做的唯一的事情就是擴展它。
// 可變模板參數
// 參數類型可變
// 參數個數可變// 打印參數包內容// template <class... Args>
// void Print(Args... args)
// {
// 可變參數模板編譯時解析
//
// 下面是運行獲取和解析,所以不支持這樣用
// cout << sizeof...(args) << endl;
// for (size_t i = 0; i < sizeof...(args); i++)
// {
// cout << args[i] << " "; // 不支持這樣用
// }
// cout << endl;
// }
2.2.1 直接擴展
template<class... Args>
void Print(Args... args) {// 完全展開:生成與args數量相同的參數SomeFunc(args...);
}Print(1, "abc", 2.0);
// 展開為:SomeFunc(1, "abc", 2.0);
2.2.2 遞歸擴展
模板是寫給編譯器的。
#include <iostream>
#include <string>using namespace std;void ShowList() {// 編譯器時遞歸的終止條件,參數包是0個時,直接匹配這個函數cout << endl;
}// 傳過來的args,是N個參數的參數包
// 調用ShowList,參數包的第一個傳給x,剩下N-1傳給第二個參數包
template <class T, class... Args>
void ShowList(T x, Args... args) {cout << x << " ";ShowList(args...); // 觸發擴展操作
}// 編譯時遞歸推導解析參數
template <class... Args>
void Print(Args... args) {ShowList(args...); // 觸發擴展操作
}int main() {Print(1, string("xxxxx"), 2.2);return 0;
}// Print(1, string("xxxxx"), 2.2);調用時
// 本質編譯器將可變參數模板通過模式的包擴展,編譯器推導的以下三個重載函數函數
// void ShowList(double x)
// {
// cout << x << " ";
// ShowList();
// }
//
// void ShowList(string x, double z)
// {
// cout << x << " ";
// ShowList(z);
// }
//
// void ShowList(int x, string y, double z)
// {
// cout << x << " ";
// ShowList(y, z);
// }
//
// void Print(int x, string y, double z)
// {
// ShowList(x, y, z);
// }
2.2.3?函數調用式包擴展
和直接擴展相比,可以對每個參數預處理。
template <class T>
const T& GetArg(const T& x) {cout << x << " ";return x;
}template <class ...Args>
void Arguments(Args... args) {}template <class ...Args>
void Print(Args... args) {// 注意GetArg必須返回接收到的對象,這樣才能組成參數包給ArgumentsArguments(GetArg(args)...);
}// void Print(int x, string y, double z)
// {
// }
// 本質可以理解為編譯器編譯時,包的擴展模式
// 將上面的函數模板擴展實例化為下面的函數
// 是不是很抽象,C++11以后,只能說委員會的大佬設計語法思維跳躍得太厲害
// Arguments(GetArg(x), GetArg(y), GetArg(z));int main() {Print(1, string("xxxxx"), 2.2);return 0;
}
2.3 emplace系列接口
template <class... Args> void emplace_back (Args&&... args);
template <class... Args> iterator emplace (const_iterator position
, Args&&... args);
C++11 以后 STL 容器新增了 emplace 系列的接口,emplace 系列的接口均為可變參數模板,功能上兼容 push 和 insert 系列。假設容器為 container<T>,emplace 還支持直接插入構造 T 對象的參數,可以直接在容器空間上構造 T 對象,高效一些。
下面我們模擬實現了 list 的 emplace 和 emplace_back 接口,這里把參數包不斷往下傳遞,最終在節點的構造中直接去匹配容器存儲的數據類型 T 的構造,可以直接在容器空間上構造 T 對象。
傳遞參數包過程中,如果是Args&&... args的萬能引用參數包,要用完美轉發參數包,方式如下
std::forward<Args>(args)...,std::forward
?分別應用到?args
?中的每一個參數上。否則編譯時包擴展后右值引用變量表達式就變成了左值。?
emplace直接在容器內構造對象,避免臨時對象的創建和拷貝/移動,因此在多數情況下比push/insert更高效。
如:下面的push_back和insert沒有實現萬能引用,對于list<pair<int,int>>,push_back({1,2})可以類型轉換,萬能引用就不能類型轉換(不知道{1,2}是什么類型),更靈活。
#pragma once
#include<assert.h>namespace Lzc
{template<class T>struct list_node{T _data;list_node<T>* _next;list_node<T>* _prev;list_node() = default;template <class... Args>list_node(Args&&... args): _next(nullptr), _prev(nullptr), _data(std::forward<Args>(args)...){}};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* node):_node(node){}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}Self& operator++(){_node = _node->_next;return *this;}Self& operator--(){_node = _node->_prev;return *this;}Self operator++(int){Self tmp(*this);_node = _node->_next;return tmp;}Self& operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const Self& s) const{return _node != s._node;}bool operator==(const Self& s) const{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 _head->_next;}iterator end(){return _head;}const_iterator begin() const{return _head->_next;}const_iterator end() const{return _head;}void empty_init(){_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;}list(){empty_init();}list(initializer_list<T> il){empty_init();for (auto& e : il){push_back(e);}}// lt2(lt1)list(const list<T>& lt){empty_init();for (auto& e : lt){push_back(e);}}// lt1 = lt3list<T>& operator=(list<T> lt){swap(lt);return *this;}~list(){clear();delete _head;_head = nullptr;}void clear(){auto it = begin();while (it != end()){it = erase(it);}}void swap(list<T>& lt){std::swap(_head, lt._head);std::swap(_size, lt._size);}void push_back(const T& x){insert(end(), x);}void push_back(T&& x){insert(end(), forward<T>(x));}// 萬能引用/*template<class X>void push_back(X&& x){insert(end(), forward<X>(x));}*/template <class... Args>void emplace_back(Args&&... args){insert(end(), std::forward<Args>(args)...);}void push_front(const T& x){insert(begin(), x);}iterator insert(iterator pos, const T& x){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(x);// prev newnode curnewnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;prev->_next = newnode;++_size;return newnode;}iterator insert(iterator pos, T&& x){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(forward<T>(x));// prev newnode curnewnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;prev->_next = newnode;++_size;return newnode;}// 萬能引用//template<class X>//iterator insert(iterator pos, X&& x)//{// Node* cur = pos._node;// Node* prev = cur->_prev;// Node* newnode = new Node(forward<X>(x));// // prev newnode cur// newnode->_next = cur;// cur->_prev = newnode;// newnode->_prev = prev;// prev->_next = newnode;// ++_size;// return newnode;//}template <class... Args>iterator insert(iterator pos, Args&&... args){Node* cur = pos._node;Node* newnode = new Node(std::forward<Args>(args)...);Node* prev = cur->_prev;// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return iterator(newnode);}void pop_back(){erase(--end());}void pop_front(){erase(begin());}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;--_size;return next;}size_t size() const{return _size;}bool empty() const{return _size == 0;}private:Node* _head;size_t _size;};
}
3、類的新功能
3.1?默認的移動構造和移動賦值
原來 C++ 類中,有 6 個默認成員函數:構造函數 / 析構函數 / 拷貝構造函數 / 拷貝賦值重載 / 取地址重載 /const 取地址重載,最重要的是前 4 個,后兩個用處不大,默認成員函數就是我們不寫編譯器會生成一個默認的。C++11 新增了兩個默認成員函數,移動構造函數和移動賦值運算符重載。
如果你自己沒有實現移動構造函數,且沒有實現析構函數、拷貝構造、拷貝賦值重載中的任意一
個(因為這幾個是綁定到一起的,都不寫,說明默認生成的就夠用了)。那么編譯器會自動生成一個默認移動構造。默認生成的移動構造函數,對于內置類型成員會執
行逐成員按字節拷貝(淺拷貝),自定義類型成員,如果實現了移動構造就調用移動構造,沒有實現就調用拷貝構造。
如果你自己沒有實現移動賦值重載函數,且沒有實現析構函數、拷貝構造、拷貝賦值重載中的任意
一個,那么編譯器會自動生成一個默認移動賦值。默認生成的移動賦值函數,對于內置類型成員會
執行逐成員按字節拷貝(淺拷貝),自定義類型成員,則需要看這個成員是否實現移動賦值,如果實現了就調用移動賦值,沒有實現就調用拷貝賦值。(默認移動賦值與移動構造完全類似)
如果你自己實現了移動構造或者移動賦值,編譯器不會自動提供拷貝構造和拷貝賦值。
3.2?成員變量聲明時給缺省值
C++初階——類和對象(下)-CSDN博客
3.3 default和delete
C++11 可以讓你更好地控制要使用的默認函數。假設你要使用某個默認的函數,但是因為一些原因這個函數沒有默認生成,可以使用?default 關鍵字顯式指定生成,比如:我們提供了拷貝構造,就不會生成移動構造了,那么我們可以使用 default 關鍵字顯式指定移動構造生成。
C++11如果想要限制某些默認函數的生成,只需在該函數聲明加上 = delete ,該語法指示編譯器不生成對應函數的默認版本,稱 = delete 修飾的函數為刪除函數。
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(Person&& p) = default;// Person(const Person& p) = delete;private:bit::string _name;int _age;
};int main() {Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;
}
3.4 final和override
C++進階——多態-CSDN博客