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