C++11中可變模板參數(shù)的實現(xiàn)
C++11的新特性可變參數(shù)模板能夠讓您創(chuàng)建可以接受可變參數(shù)的函數(shù)模板和類模板,相比 C++98/03,類模版和函數(shù)模版中只能含固定數(shù)量的模版參數(shù),可變模版參數(shù)無疑是一個巨大的改 進。
像之前學(xué)習(xí)的printf就是一個函數(shù)參數(shù)的可變參數(shù),它可以接收多個任意類型,但它們只函數(shù)參數(shù)的可變參數(shù),并不是模板的可變參數(shù)
printf的使用方法:
int printf( const char *format , ... );
本博客講解的是函數(shù)模板的可變參數(shù),不會涉及到類模板的可變參數(shù)
可變模板的定義方式
函數(shù)的可變參數(shù)模板定義方式如下:
template<class ...Args> //Args全稱:arguments 返回類型 函數(shù)名(Args... args) { //函數(shù)體 }
下面就是一個基本可變參數(shù)的函數(shù)模板
template <class ...Args> void ShowList(Args... args) {}
Args:是一個可變模板參數(shù)包
args:是一個函數(shù)形參參數(shù)包
說明一下:
模板參數(shù)Args前面有省略號,代表它是一個可變模板參數(shù),我們將帶省略號的參數(shù)稱為 “參數(shù)包”,這個參數(shù)包中可以包含0到任意個模板參數(shù),args則是一個函數(shù)形參參數(shù)包
現(xiàn)在我們可以向這個函數(shù)中傳入多個不同的類型,并且可以通過sizeof算出參數(shù)包的參數(shù)個數(shù)
以下例代碼為例:
template<class ...Args> void ShowList(Args... args) { cout << sizeof...(args) << endl; } int main() { ShowList(1); ShowList(1, 2); ShowList(1, 2, string("dict")); map<string, int> m1; ShowList(1, 2, 3, m1); return 0; }
我們無法直接獲取參數(shù)包args中的每個參數(shù)的, 只能通過展開參數(shù)包的方式來獲取參數(shù)包中的每個參數(shù),這是使用可變模版參數(shù)的一個主要特點,也是最大的難點,即如何展開可變模版參數(shù)。
由于C++11語法不支持使用args[i]這樣方式獲取可變q參數(shù),所以我們的用一些奇招來一一獲取參數(shù)包的值。
錯誤示例:
template<class ...Args> void ShowList(Args... args) { //error for (int i = 0; i < sizeof...(args); ++i) { cout << args[i] << endl; } }
參數(shù)包的展開方式
遞歸的方式展開參數(shù)包
方式如下:
1.給函數(shù)模板新增一個參數(shù),這樣就可以從接收到的參數(shù)包分離出來一個參數(shù)
2.在函數(shù)模板中進行遞歸,不斷的分離參數(shù)包中的參數(shù)
3.直到接收到最后一個參數(shù)結(jié)束
結(jié)束條件;
->1. 可以創(chuàng)建一個無參的函數(shù)來終止遞歸:當(dāng)參數(shù)包中的參數(shù)為0時會調(diào)用該函數(shù)終止循環(huán)
void _ShowList() { cout << endl; } template<class T, class ...Args> void _ShowList(T value, Args... args) { cout << value << ' '; _ShowList(args...); } int main() { _ShowList(1, 2, string("dict")); return 0; }
->2. 可以創(chuàng)建一個參數(shù)的函數(shù)來終止遞歸:當(dāng)參數(shù)包中的參數(shù)為1時會調(diào)用該函數(shù)終止循環(huán)
template<class T> void _ShowList(const T& t) { cout << t << endl; } template<class T, class ...Args> void _ShowList(T value, Args... args) { cout << value << ' '; _ShowList(args...); } int main() { _ShowList(1, 2, string("dict")); return 0; }
但是使用該方法有一個弊端:我們在調(diào)用ShowList函數(shù)時必須至少傳入一個參數(shù),否則就會報錯,因為此時無論是調(diào)用遞歸終止函數(shù)還是展開函數(shù),都需要至少傳入一個參數(shù)
使用sizeof...(args)算出參數(shù)個數(shù)的特性,利用它的特性做一個遞歸結(jié)束條件可以嗎?不行!
template<class T, class ...Args> void ShowList(T value, Args... args) { cout << value << ' '; if (sizeof...(args)) { return; } ShowList(args...); }
函數(shù)模板并不能調(diào)用,函數(shù)模板需要在編譯時根據(jù)傳入的實參類型進行推演,生成對應(yīng)的函數(shù),這個生成的函數(shù)才能夠被調(diào)用。
而這個推演過程是在編譯時進行的,當(dāng)推演到參數(shù)包args中參數(shù)個數(shù)為0時,還需要將當(dāng)前函數(shù)推演完畢,這時就會繼續(xù)推演傳入0個參數(shù)時的ShowList函數(shù),此時就會產(chǎn)生報錯,因為ShowList函數(shù)要求至少傳入一個參數(shù)。
這里編寫的if判斷是在代碼編譯結(jié)束后,運行代碼時才會所走的邏輯,也就是運行時邏輯,而函數(shù)模板的推演是一個編譯時邏輯。
還有一種特殊的方式,該方法比較抽象,就是使用逗號表達式展開參數(shù)包
->3. 逗號表達式展開參數(shù)包
template<class T> void CPPprint(const T& value) { cout << value << ' '; } template<class ...Args> void ShowList(Args... args) { int array[] = {( CPPprint(args), 0)...}; cout << endl; }
當(dāng)我們在數(shù)組中不標注元素個數(shù)時,編譯器會幫我們自動推導(dǎo)元素個數(shù),這時它會幫我們展開參數(shù)包
如下:
int array[] = {( CPPprint(args), 0), CPPprint(args), 0), CPPprint(args), 0), CPPprint(args), 0)};
在調(diào)用CPPprint函數(shù)的同時,利用逗號運算符的特性進行對數(shù)組的初始化
其實也可以不使用逗號運算符完成該操作
template<class T> int CPPprint(const T& value) { cout << value << ' '; return 0; } template<class ...Args> void ShowList(Args... args) { int array[] = { (CPPprint(args))... }; cout << endl; }
將被調(diào)用的函數(shù)設(shè)置一個返回值,調(diào)用之后返回0,這樣就可以在編譯器展開參數(shù)包調(diào)用函數(shù)時,通過返回值初始化
STL中的emplace相關(guān)接口函數(shù)
以便大家更好的理解emplace,先給大家看一段代碼,可變模板參數(shù)的使用場景:
class Date { public: Date(int year = 1, int month = 1, int day = 1) :_year(year) , _month(month) , _day(day) { cout << "Data()~構(gòu)造函數(shù)" << endl; } Date(const Date& d) :_year(d._year) , _month(d._month) , _day(d._day) { cout << "Date()~拷貝構(gòu)造" << endl; } private: int _year; int _month; int _day; }; template<class ...Args> Date* Init(Args&&... args) { Date* ret = new Date(args...); return ret; } int main() { Date* p1 = Init(); Date* p2 = Init(2024); Date* p3 = Init(2024, 11); Date* p4 = Init(2024, 11, 12); Date d1(2, 3, 3); Date* p5 = Init(d1); return 0; }
我們通過將參數(shù)傳入?yún)?shù)包在編譯期間通過將參數(shù)包展開的操作進行對象的構(gòu)造
STL容器中emplace相關(guān)插入接口函數(shù)
C++11標準STL中的容器增加emplace版本的插入接口,比如list容器的push_front,push_back和insert函數(shù),都增加了對應(yīng)的emplace_front,emplace_back,emplace函數(shù)。如下:
emplace接口全部都是使用的可變參數(shù)模板
注意:兩個&&是萬能引用并不是右值引用
對比list中的push_back和emplace_back,對于emplace系列接口而言,它的主要優(yōu)勢就是直接在容器內(nèi)部構(gòu)造元素可以結(jié)合我上面給的場景進行理解,而不是構(gòu)造一個臨時對象在復(fù)制或移動到容器中可以有效的避免拷貝和移動操作
以emplace和push_back為例:
調(diào)用push_back函數(shù)插入元素時,可以傳入左值對象或者右值對象,也可以使用列表初始化
調(diào)用emplace時可以傳左值對象或者右值對象,但是不能使用列表初始化,emplace系列最大的特點就是,插入元素時可以傳入用于構(gòu)造元素的參數(shù)包
比如:
int main() { list<pair<nxbw::string, int>> mylist; pair<nxbw::string, int> kv("nxbw", 10); mylist.emplace_back(kv); //傳左值 mylist.emplace_back(make_pair("nxbw", 10)); //傳右值 mylist.emplace_back("nxbw", 10); //傳參數(shù)包 mylist.push_back(kv); //傳左值 mylist.push_back(make_pair("nxbw", 10)); //傳右值 mylist.push_back({ "nxbw", 10 }); //使用列表初始化 return 0; }
原地構(gòu)造:使用emplace,你可以提供構(gòu)造元素所需的參數(shù),容器會直接在emplace接口的實現(xiàn)中構(gòu)造該對象
emplace系列接口的工作流程
emplace系列接口的工作流程如下:
- 先通過空間配置器為新結(jié)點獲取一塊內(nèi)存空間,注意這里只會開辟空間,不會自動調(diào)用構(gòu)造函數(shù)對這塊空間進行初始化。
- 然后調(diào)用allocator_traits::construct函數(shù)對這塊空間進行初始化,調(diào)用該函數(shù)時會傳入這塊空間的地址和用戶傳入的參數(shù)(需要經(jīng)過完美轉(zhuǎn)發(fā))。
- 在allocator_traits::construct函數(shù)中會使用定位new表達式,顯示調(diào)用構(gòu)造函數(shù)對這塊空間進行初始化,調(diào)用構(gòu)造函數(shù)時會傳入用戶傳入的參數(shù)(需要經(jīng)過完美轉(zhuǎn)發(fā))。
- 將初始化好的新結(jié)點插入到對應(yīng)的數(shù)據(jù)結(jié)構(gòu)當(dāng)中,比如list容器就是將新結(jié)點插入到底層的雙鏈表中。
emplace系列接口的意義
由于emplace系列接口的可變模板參數(shù)的類型都是萬能引用,因此既可以接收左值對象,也可以接收右值對象,還可以接收參數(shù)包。
- 如果調(diào)用emplace系列接口時傳入的是左值對象,那么首先需要先在此之前調(diào)用構(gòu)造函數(shù)實例化出一個左值對象,最終在使用定位new表達式調(diào)用構(gòu)造函數(shù)對空間進行初始化時,會匹配到拷貝構(gòu)造函數(shù)。
- 如果調(diào)用emplace系列接口時傳入的是右值對象,那么就需要在此之前調(diào)用構(gòu)造函數(shù)實例化出一個右值對象,最終在使用定位new表達式調(diào)用構(gòu)造函數(shù)對空間進行初始化時,就會匹配到移動構(gòu)造函數(shù)。
- 如果調(diào)用emplace系列接口時傳入的是參數(shù)包,那就可以直接調(diào)用函數(shù)進行插入,并且最終在使用定位new表達式調(diào)用構(gòu)造函數(shù)對空間進行初始化時,匹配到的是構(gòu)造函數(shù)。
總結(jié)一下:
- 傳入左值對象,需要調(diào)用構(gòu)造函數(shù)+拷貝構(gòu)造函數(shù)。
- 傳入右值對象,需要調(diào)用構(gòu)造函數(shù)+移動構(gòu)造函數(shù)。
- 傳入?yún)?shù)包,只需要調(diào)用構(gòu)造函數(shù)。
當(dāng)然,這里的前提是容器中存儲的元素所對應(yīng)的類,是一個需要深拷貝的類,并且該類實現(xiàn)了移動構(gòu)造函數(shù)。否則在調(diào)用emplace系列接口時,傳入左值對象和傳入右值對象的效果都是一樣的,都需要調(diào)用一次構(gòu)造函數(shù)和一次拷貝構(gòu)造函數(shù)。
實際emplace系列接口的一部分功能和原有各個容器插入接口是重疊的,因為容器原有的push_back、push_front和insert函數(shù)也提供了右值引用版本的接口,如果調(diào)用這些接口時如果傳入的是右值對象,那么最終也是會調(diào)用對應(yīng)的移動構(gòu)造函數(shù)進行資源的移動的。
emplace接口的意義:
emplace系列接口最大的特點就是支持傳入?yún)?shù)包,用這些參數(shù)包直接構(gòu)造出對象,這樣就能減少一次拷貝,這就是為什么有人說emplace系列接口更高效的原因。
但emplace系列接口并不是在所有場景下都比原有的插入接口高效,如果傳入的是左值對象或右值對象,那么emplace系列接口的效率其實和原有的插入接口的效率是一樣的。
emplace系列接口真正高效的情況是傳入?yún)?shù)包的時候,直接通過參數(shù)包構(gòu)造出對象,避免了中途的一次拷貝。
通過下面的場景我們來驗證一下:
namespace nxbw { 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); } // s1.swap(s2) 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; } // 移動構(gòu)造 string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- 移動語義" << endl; swap(s); } // 移動賦值 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移動語義" << endl; swap(s); 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) string& operator+=(char ch) { push_back(ch); return *this; } const char* c_str() const { return _str; } private: char* _str; size_t _size; size_t _capacity; // 不包含最后做標識的\0 }; }
int main() { list<pair<nxbw::string, int>> mylist; pair<nxbw::string, int> kv("nxbw", 10); //構(gòu)造 mylist.emplace_back(kv); //傳左值, mylist.emplace_back(pair<nxbw::string, int>("nxbw", 10)); //傳右值 mylist.emplace_back("nxbw", 10); //傳參數(shù)包 return 0; }
由于我們在string的構(gòu)造函數(shù)、拷貝構(gòu)造函數(shù)和移動構(gòu)造函數(shù)當(dāng)中均打印了一條提示語句,因此我們可以通過控制臺輸出來判斷這些函數(shù)是否被調(diào)用。
下面我們用一個容器來存儲模擬實現(xiàn)的string,并以不同的傳參形式調(diào)用emplace系列函數(shù)。比如:
說明一下:
模擬實現(xiàn)string的拷貝構(gòu)造函數(shù)時復(fù)用了構(gòu)造函數(shù),因此在調(diào)用string拷貝構(gòu)造的后面會緊跟著調(diào)用一次構(gòu)造函數(shù)。
為了更好的體現(xiàn)出參數(shù)包的概念,因此這里list容器中存儲的元素類型是pair,我們是通過觀察string對象的處理過程來判斷pair的處理過程的。
這里也可以以不同的傳參方式調(diào)用push_back函數(shù),順便驗證一下容器原有的插入函數(shù)的執(zhí)行邏輯。比如:
int main() { list<pair<nxbw::string, int>> mylist; pair<nxbw::string, int> kv("nxbw", 10); mylist.push_back(kv); //傳左值 mylist.push_back(pair<nxbw::string, int>("nxbw", 10)); //傳右值 mylist.push_back({ "nxbw", 10 }); //使用列表初始化 return 0; }
模擬實現(xiàn):emplace接口
namespace nxbw { // 模擬實現(xiàn)list在之前的章節(jié)有提過,這里只是將原來的代碼多增加一些接口的片段代碼 // 這是list需要用到的節(jié)點類 template<class T> struct __list_node { __list_node(const T& val = T()) :_data(val), _prev(nullptr), _next(nullptr) {} // 這里需要在原來的基礎(chǔ)上需要增加一個可變模板參數(shù)模板的構(gòu)造函數(shù),方便下面使用new template<class ...Args> __list_node(Args&& ...args) : _data(std::forward<Args>(args)...), _prev(nullptr), _next(nullptr) {} T _data; __list_node* _prev; __list_node* _next; }; template<class T> struct list { template<class ...Args> iterator emplace(iterator position, Args&&... args) { node* cur = position._node; node* prev = cur->_prev; // 函數(shù)參數(shù)包的完美轉(zhuǎn)發(fā) node* newnode = new node(forward<Args>(args)...); prev->_next = newnode; newnode->_prev = prev; newnode->_next = cur; cur->_prev = newnode; return iterator(cur); } template<class ...Args> void emplace_back(Args&&... args) { // 函數(shù)參數(shù)包的完美轉(zhuǎn)發(fā) emplace(end(), forward<Args>(args)...); } // 獲取節(jié)點函數(shù),這里更新成了萬能引用版的 template<class T> node* get_node(T&& val = T()) { node* new_node = new node(forward<T>(val)); // 完美轉(zhuǎn)發(fā) new_node->_prev = new_node; new_node->_next = new_node; return new_node; } private: __list_node<T>* _head; // 指向節(jié)點類的指針 }; };
emplace系列和push_back以及insert的區(qū)別
效率方面:對于左值引用版本的push_back和insert來說確實有很大的效率提升,對于右值引用版本的push_back和insert來說效率其實差不多,因為移動賦值/拷貝代價足夠小
構(gòu)造復(fù)雜對象:當(dāng)元素的構(gòu)造比叫復(fù)雜時,emplace可以讓代碼更簡潔,直接傳入構(gòu)造參數(shù)即可
到此這篇關(guān)于C++11中可變模板參數(shù)的實現(xiàn)的文章就介紹到這了,更多相關(guān)C++11 可變模板參數(shù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
教你如何使用qt quick-PathView實現(xiàn)好看的home界面
pathView的使用類似與ListView,都需要模型(model)和代理(delegate),只不過pathView多了一個路徑(path)屬性,顧名思義路徑就是item滑動的路徑,下面給大家分享qt quick-PathView實現(xiàn)好看的home界面,一起看看吧2021-06-06淺析C/C++ 中return *this和return this的區(qū)別
return *this返回的是當(dāng)前對象的克隆或者本身,return this返回當(dāng)前對象的地址,下面通過本文給大家介紹C/C++ 中return *this和return this的區(qū)別,感興趣的朋友一起看看吧2019-10-10C語言數(shù)據(jù)結(jié)構(gòu)與算法之排序總結(jié)(二)
這篇文章住要介紹的是選擇類排序中的簡單、樹形和堆排序,歸并排序、分配類排序的基數(shù)排序,文中的示例代碼講解詳細,感興趣的小伙伴可以了解一下2021-12-12