?????????Hello大家好!很高興我們又見面啦!給生活添點passion,開始今天的編程之路!
我的博客:<但凡.
我的專欄:《編程之路》、《數據結構與算法之美》、《題海拾貝》、《C++修煉之路》
歡迎點贊,關注!
目錄
1、可變參數模板
? ? ? ? 1.1、基本語法
? ? ? ? 1.2、包擴展
? ? ? ? 1.3、emplace系列接口
2、類的新功能
? ? ? ? 2.1、默認的移動構造和移動賦值
? ? ? ? 2.2、委托構造
? ? ? ? 2.3、default和delete
1、可變參數模板
????????可變參數模板(Variadic Templates)是C++11引入的一個重要特性,它允許模板接受任意數量和類型的參數。
? ? ? ? 1.1、基本語法
? ? ? ? 可變參數模板可以使函數模板或者類模板支持任意多個類型的變量。可變數目的參數被稱為參數包。存在兩種參數包,第一種是模板參數包,第二種是函數參數包。參數包可以接受0個參數。
template<class ...Args> void func(Args... args) {}
template<class ...Args> void func(Args&... args) {}
template<class ...Args> void func(Args&&... args) {}
? ? ? ? 比如,在上面三個函數模板中,Arges是模板參數包,arges是函數參數包。我們需要注意一下格式,注意一下三個點的位置。
? ? ? ? 對于上面的三個函數模板,第三個函數模板我們使用的是右值引用,這意味著對于參數包中的每個類型都是使用的萬能引用。如果傳左值,這個參數包中的類型被推導為左值引用,引用折疊后為左值引用。如果傳右值的話,類型被推導為右值引用,折疊后為右值引用。
????????可變參數模板的原理跟模板類似,本質還是去實例化對應類型和個數的多個函數。
? ? ? ? 我們可以使用sizeof...來計算參數包中參數的個數。
#include<iostream>
using namespace std;template<class ...Args>
void func(Args... args)
{cout << sizeof...(args) << endl;
}
int main()
{func();//0func(1);//1func(1, 2);//2func(1, 2,"sss");//3return 0;
}
? ? ? ? 這個計算也是編譯時計算,因為本質上我們就是實例化出四個函數。其實參數包也是使模板進一步的泛型化。
? ? ? ? 本質上是替換了這四個函數:
void func()
{}
void func(int a)
{}
void func(int a,int b)
{}
void func(int a,int b,const char* str)
{}
? ? ? ? 1.2、包擴展
? ? ? ? 現在我們實現一個print,對于傳進函數的每一個變量都打印一次。
? ? ? ? ?注意參數包不能這樣用,因為參數包不是一個容器:
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;
//}
? ? ? ? 那么我們怎么實現print函數呢?我們可以使用包擴展來實現。
? ? ? ? 包擴展是在編譯時進行的。包擴展本質上是遞歸調用,但是是在編譯時遞歸。
void ShowList()
{// 編譯器時遞歸的終?條件,參數包是0個時,直接匹配這個函數 cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{cout << x << " ";// args是N個參數的參數包 // 調?ShowList,參數包的第?個傳給x,剩下N-1傳給第?個參數包 ShowList(args...);
}
// 編譯時遞歸推導解析參數
template <class ...Args>
void Print(Args... args)
{ShowList(args...);//注意傳參的時候三個點又寫到后面了
}
int main()
{Print();Print(1);Print(1, "xxxxx");Print(1, "xxxxx", 2.2);return 0;
}
? ? ? ? 我們把參數傳給print,然后print在調用showlist,每次“剔除”一個變量。知道參數包中變量數為0,此時再去調用showlist直接調用最上面的showlist函數。
? ? ? ? 正因為是編譯時調用,所以我們不能這樣寫:?
template <class T, class ...Args>
void ShowList(T x, Args... args)
{if (sizeof...(args) == 0){return;}cout << x << " ";// args是N個參數的參數包 // 調?ShowList,參數包的第?個傳給x,剩下N-1傳給第?個參數包 ShowList(args...);
}
? ? ? ? 因為這個函數結束條件是運行時判斷邏輯。?
? ? ? ? 還有一種包擴展的方式:
template <class T>
const T& GetArg(const T& x)
{cout << x << " ";return x;
}
template <class ...Args>
void Arguments(Args... args)
{cout << endl;
}
template <class ...Args>
void Print(Args... args)
{// 注意GetArg必須返回或者到的對象,這樣才能組成參數包給Arguments Arguments(GetArg(args)...);
}
? ? ? ? 我們把傳入print的幾個參數組成參數包傳給GetArg。對于GetArg來說,我們相當于把參數包中的每個參數都傳給GetArg,然后把GetArg的所有返回值在組成一個參數包,傳給Arguments.?這種包擴展就是普通的編譯推導,并不是遞歸。
? ? ? ? 需要注意的是上面這種的包擴展方式在vs上是倒序輸出的,也就是說輸出結果和我們的傳參順序恰好相反。造成這種結果的原因是C++標準沒有規定函數參數的求值順序,而大部分編譯器默認是從右到左。
? ? ? ? 1.3、emplace系列接口
? ? ? ? C++11之后所有的stl容器都新增了emplace接口,empalce系列的接口均為模板可變參數。接下來我們使用list來測試一下emplace系列的接口。
?????????
? ? ? ? 對于emplace_back和push_back來說,無論是傳左值,還是傳右值,效率都沒有區別。唯一有區別的場景就是這種:
int main()
{list<string> li;li.push_back("sss");//構造加移動構造li.emplace_back("sss");//直接構造return 0;
}
? ? ? ? 對于push_back來說,因為push_back這個函數的形參就是string&&類型的,如果我們想讓形參和實參匹配上,需要先對形參進行構造,構造一個臨時對象,然后再移動構造給string對象進行push_back。
? ? ? ? 但是對于emplace_back來說,由于他是一個模板,他可以直接接受const char*類型的變量,然后再在插入之前進行一次構造,構造出string對象進行插入就可以了。
? ? ? ? ?接下來我們看下面這個場景:
#include<iostream>
#include<list>
#include<string>
using namespace std;
int main()
{list<pair<int,double>> li;li.push_back({ 6,5.5 });//li.emplace_back({ 6,5.5 });li.emplace_back(6, 5.5);return 0;
}
?????????對于pair類型,我們使用emplace接口時不能傳花括號,也就是說不能傳初始化列表。因為咱們的emplace_back是模板函數,所以在傳參是他會去推導類型。由于initializer_list在推導時必須是initializer_list<T>,也就是說列表中的值類型必須是相同的。但是這里一個int,一個double不是同一類型參數,所以說會編譯報錯。
? ? ? ? 但是對于直接傳構造底層對象的參數是沒有問題的。這也是emplace系列接口的正確用法:在插入值時向emplace系列接口中直接傳入構造底層存儲對象類型所需的參數。這樣相比普通插入效率更高。但如果容器存儲的是一些基本類型(如int,double,char)時使用emplace_back或push_back效率上沒有差異。
? ? ? ? 現在我們對之前模擬實現的list進行一下升級,寫一下emplace系列接口:
......
list_node(T&& x):_next(nullptr), _prev(nullptr), _data(std::move(x))
{}
//如果上面兩個構造函數都給了默認值=T(),那么當new node時,也就是不傳參是無法確定匹配哪個構造函數。
//一般右值版本不給默認值。右值引用通常用于移動語義,而默認構造的臨時對象(T())不適合被移動。
// 語義上矛盾:右值引用表示要"竊取"資源,但默認參數會創建一個新對象
template<class ...Args>
list_node(Args&&... args): _next(nullptr), _prev(nullptr), _data(std::forward<Args>(args)...)
{}
......
template<class ...Args>
void emplace_back(Args&&... args)
{emplace(end(), std::forward<Args>(args)...);
}
template<class ...Args>
void emplace(iterator pos, Args&&... args)
{Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(std::forward<Args>(args)...);// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;
}
?????????注意的是我們的參數包構造不需要解析參數包,也就是說編譯器不需要進行包擴展什么的,編譯器不需要一個一個看都是什么類型,而是直接拿去和list底層存儲類型的構造函數匹配。如果匹配對不上就報錯。
? ? ? ? 另外就是每次傳參數包我們都需要完美轉發。讓參數包中的參數都保持原有屬性。? ? ? ? ? ? ? ? ?
2、類的新功能
? ? ? ? 2.1、默認的移動構造和移動賦值
? ? ? ? C++11新增了兩個默認成員函數:移動構造函數和移動賦值運算符重載。
? ? ? ? 如果你沒有自己生成移動構造函數,且沒有實現析構函數,拷貝構造,拷貝賦值重載中的任意一個,那么編譯器就會自動生成一個默認移動構造。默認生成的移動構造函數,對于內置類型成員會執行逐成員按字節拷貝,自定義類型成員,則需要看這個成員是否實現移動構造,如果實現了就調用移動構造,沒有實現就調用拷貝構造。
? ? ? ? 同理,如果你沒有自己實現移動賦值重載函數,且沒有實現析構函數、拷貝構造、拷貝賦值重載中的任意一個,那么編譯器會自動生成一個默認移動賦值。默認生成的移動構造函數,對于內置類型成員會 執行逐成員按字節拷貝,自定義類型成員,則需要看這個成員是否實現移動賦值,如果實現了就調用移動賦值,沒有實現就調用拷貝賦值。
????????如果你提供了移動構造或者移動賦值,編譯器不會自動提供拷貝構造和拷貝賦值。
? ? ? ? 2.2、委托構造
class A
{
public:A(int a, int b):_a(a),_b(b){}A(int a, int b, char c):A(a, b){_c = c;}
private:int _a;int _b;char _c=0;
};
? ? ? ? 上面這個類就實現了委托構造,本質上也是一種復用。?
? ? ? ? 2.3、default和delete
? ? ? ? default可以強制讓編譯器生成默認函數:
class MyClass {
public:MyClass() = default; // 顯式要求編譯器生成默認構造函數MyClass(const MyClass&) = default; // 默認拷貝構造函數
};
? ? ? ? delete就與default相反,不讓編譯器生成某個默認函數:
class NonCopyable {
public:NonCopyable(const NonCopyable&) = delete;NonCopyable& operator=(const NonCopyable&) = delete;
};
? ? ? ? ?好了,今天的內容就分享到這,我們下期再見!