C++可變參數(shù)模板深入深剖
概念
C++11 新增一員猛將就是可變參數(shù)模板,他可以允許可變參數(shù)的函數(shù)模板和類模板來作為參數(shù),使得參數(shù)高度泛化。
在 C++11 之前類模板和函數(shù)模板中只能包含固定數(shù)量模板參數(shù),而且也有可變參數(shù)的概念,比如 printf 函數(shù)就能夠接收任意多個(gè)參數(shù),但這是函數(shù)參數(shù)的可變參數(shù),并不是模板的可變參數(shù)。可變模板參數(shù)無疑是一個(gè)巨大的改進(jìn),但由于可變參數(shù)模板比較抽象,因此使用起來并不會(huì)太簡單。
模板定義
函數(shù)的可變參數(shù)模板定義方式如下:
template<class …Args> 返回類型 函數(shù)名(Args… args) { ??//函數(shù)體 }
比如:
template<class ...Args> void ShowList(Args... args) {}
注意這里的書寫格式,模板參數(shù)Args前面有省略號,代表它是一個(gè)可變模板參數(shù), 我們把帶省略號的參數(shù)稱為參數(shù)包 \color{red} {我們把帶省略號的參數(shù)稱為參數(shù)包} 我們把帶省略號的參數(shù)稱為參數(shù)包,參數(shù)包里面可以包含0到 N(N≥0) 個(gè)模板參數(shù), 而 a r g s 則是一個(gè)函數(shù)形參參數(shù)包 \color{red} {而 args 則是一個(gè)函數(shù)形參參數(shù)包} 而args則是一個(gè)函數(shù)形參參數(shù)包。
模板參數(shù)包 Args 和函數(shù)形參參數(shù)包 args 的名字可以任意指定,并不是說必須叫做 Args 和 args 。
那么現(xiàn)在函數(shù)傳參就可以實(shí)不同類型了:
int main() { ShowList(); ShowList(1); ShowList(1, 'A'); ShowList(1, 'A', string("hello")); return 0; }
然后在函數(shù)模板中通過sizeof計(jì)算參數(shù)包中參數(shù)的個(gè)數(shù):
template<class ...Args> void ShowList(Args... args) { cout << sizeof...(args) << endl; //獲取參數(shù)包中參數(shù)的個(gè)數(shù) }
現(xiàn)在最大的難點(diǎn)就是我們無法直接獲取參數(shù)包中的每個(gè)參數(shù),語法并不支持使用 args[i] 的方式來獲取參數(shù)包中的參數(shù),只能通過展開參數(shù)包的方式來獲取,這是使用可變參數(shù)模板的一個(gè)主要特點(diǎn)。
template<class ...Args> void ShowList(Args... args) { //錯(cuò)誤示例: for (int i = 0; i < sizeof...(args); i++) { cout << args[i] << " "; //打印參數(shù)包中的每個(gè)參數(shù) } cout << endl; }
參數(shù)包展開
遞歸函開
該方法大概分為三步:
- 給函數(shù)模板增加一個(gè)模板參數(shù),從接收的參數(shù)包中分離出一個(gè)參數(shù)出來
- 在函數(shù)模板中遞歸調(diào)用該函數(shù)模板,調(diào)用時(shí)傳入剩下的參數(shù)包
- 繼續(xù)遞歸,直到參數(shù)包中所有參數(shù)都被取出來
比如:
template<class T, class ...Args> void ShowList(T value, Args... args) { cout << value << " "; //打印分離出的第一個(gè)參數(shù) ShowList(args...); //繼續(xù)遞歸調(diào)用 }
那么最后還有一個(gè)問題就是:遞歸展開該如何終止?
方法其實(shí)挺簡單就是寫一個(gè)無參的遞歸終止函數(shù),該函數(shù)的函數(shù)名與展開函數(shù)的函數(shù)名相同,如果傳入的參數(shù)包中參數(shù)個(gè)數(shù)是 0,那么就會(huì)匹配到這個(gè)無參遞歸終止函數(shù),這樣就結(jié)束了遞歸:
//遞歸終止函數(shù) void ShowList() { cout << endl; } //展開函數(shù) template<class T, class ...Args> void ShowList(T value, Args... args) { cout << value << " "; //打印分離出的第一個(gè)參數(shù) ShowList(args...); //繼續(xù)遞歸調(diào)用 }
但是外部調(diào)用 ShowList 時(shí)不會(huì)傳入?yún)?shù),就會(huì)直接匹配到無參遞歸終止函數(shù)。而我們本意是想讓外部調(diào)用 ShowList 函數(shù)時(shí)匹配到函數(shù)模板,并不是直接匹配遞歸終止函數(shù)。
因此我們可以將展開函數(shù)和遞歸調(diào)用函數(shù)的函數(shù)名改為 ShowListArg,然后重新編寫一個(gè) ShowList 函數(shù)模板,在該函數(shù)模板的函數(shù)體中要做的就是調(diào)用ShowListArg 的展開參數(shù)包 :
void ShowListArg() { cout << endl; } //展開函數(shù) template<class T, class ...Args> void ShowListArg(T value, Args... args) { cout << value << " "; ShowListArg(args...); //繼續(xù)遞歸 } //供外部調(diào)用的函數(shù) template<class ...Args> void ShowList(Args... args) { ShowListArg(args...); }
這樣無論外部調(diào)用時(shí)傳入多少個(gè)參數(shù),最終匹配到的都是同一個(gè)函數(shù)了,那么如何編寫帶參的遞歸終止函數(shù)呢
比如帶一個(gè)參數(shù)的:
template<class T> void ShowListArg(const T& t) { cout << t << endl; } //展開函數(shù) template<class T, class ...Args> void ShowListArg(T value, Args... args) { cout << value << " "; ShowList(args...); //繼續(xù)遞歸 } //供外部調(diào)用的函數(shù) template<class ...Args> void ShowList(Args... args) { ShowListArg(args...); }
但該方法有一個(gè)缺陷,在調(diào)用 ShowList 函數(shù)時(shí)至少要傳入一個(gè)參數(shù),否則就會(huì)報(bào)錯(cuò),因?yàn)榇藭r(shí)無論是調(diào)用遞歸終止函數(shù)還是展開函數(shù),都需要至少一個(gè)參數(shù),那我們能不能先計(jì)算一下參數(shù)包中的參數(shù)個(gè)數(shù)呢?
答案是:No!可能你會(huì)覺得 sizeof 這里也可以直接計(jì)算參數(shù)個(gè)數(shù),來康康 錯(cuò)誤示范 \color{red} {錯(cuò)誤示范} 錯(cuò)誤示范:
template<class T, class ...Args> void ShowList(T value, Args... args) { cout << value << " "; //打印傳入的第一個(gè)參數(shù) if (sizeof...(args) == 0) { return; } ShowList(args...); //繼續(xù)遞歸 }
首先函數(shù)模板并不能調(diào)用,函數(shù)模板需要在編譯時(shí)根據(jù)傳入的實(shí)參類型進(jìn)行推演,生成對應(yīng)的函數(shù)才能夠被調(diào)用,而這個(gè)推演過程是在編譯時(shí)進(jìn)行的,當(dāng)推演到參數(shù)包 args 中參數(shù)個(gè)數(shù)為 0 時(shí),函數(shù)不會(huì)停下會(huì)繼續(xù)推演完畢,這時(shí)就會(huì)繼續(xù)傳入 0 個(gè)參數(shù)時(shí)的 ShowList 函數(shù),此時(shí)就會(huì)報(bào)錯(cuò) ShowList 函數(shù)沒有參數(shù)。
這里編寫的 if 判斷是運(yùn)行時(shí)才跑的邏輯,也就是運(yùn)行時(shí)邏輯,而函數(shù)模板的推演是一個(gè)編譯時(shí)邏輯!
逗號表達(dá)式展開
我們知道數(shù)組可以通過列表進(jìn)行初始化。如果參數(shù)包中各個(gè)參數(shù)類型都是整型,那么也可以把這個(gè)參數(shù)包放到列表中,初始化這個(gè)整型數(shù)組,此時(shí)參數(shù)包中參數(shù)就放到數(shù)組中了:
template<class ...Args> void ShowList(Args... args) { int arr[] = { args... }; //列表初始化 //打印參數(shù)包中的各個(gè)參數(shù) for (auto e : arr) { cout << e << " "; } cout << endl; }
這樣就可以傳入多個(gè)參數(shù)了:
int main() { ShowList(1); ShowList(1, 2); ShowList(1, 2, 3); return 0; }
但 C++ 并不像 Python 一樣激進(jìn)敢秀,C++ 規(guī)定器中存儲(chǔ)的數(shù)據(jù)類型是相同的,因此調(diào)用 ShowList 時(shí)傳入的參數(shù)只能是整型,并且還不能傳入 0 個(gè)參數(shù),因?yàn)閿?shù)組的大小不能為 0,因此還需要在此基礎(chǔ)上借助逗號表達(dá)式來展開參數(shù)包
逗號表達(dá)式規(guī)則是會(huì)從左到右依次計(jì)算各個(gè)表達(dá)式,并將最后一個(gè)表達(dá)式的值作為返回值返回,我們將最后一個(gè)表達(dá)式設(shè)為整型值,確保最后返回的是一個(gè)整型。
將處理參數(shù)個(gè)數(shù)的動(dòng)作封裝成一個(gè)函數(shù),將該函數(shù)作為逗號表達(dá)式的第一個(gè)表達(dá)式
template<class T> void PrintArg(const T& t) { cout << t << " "; } //展開函數(shù) template<class ...Args> void ShowList(Args... args) { int arr[] = { (PrintArg(args), 0)... }; //列表初始化+逗號表達(dá)式 cout << endl; }
我們這里要做的就是打印參數(shù)包中的各個(gè)參數(shù),因此處理函數(shù)當(dāng)中要做的就是將傳入的參數(shù)進(jìn)行打印即可
可變參數(shù)的省略號需要加在逗號表達(dá)式外面,表示需要先將逗號表達(dá)式展開,如果直接加在 args 后面,那么參數(shù)包將會(huì)被展開后全部傳入 PrintArg ,代碼中會(huì)展開成 {(PrintArg(arg1), 0), (PrintArg(arg2), 0), (PrintArg(arg3), 0), etc…}
//支持無參調(diào)用 void ShowList() { cout << endl; } //處理函數(shù) template<class T> void PrintArg(const T& t) { cout << t << " "; } //展開函數(shù) template<class ...Args> void ShowList(Args... args) { int arr[] = { (PrintArg(args), 0)... }; //列表初始化+逗號表達(dá)式 cout << endl; }
當(dāng)然,我們也可以不使用逗號表達(dá)式,這里的問題是初始化整型數(shù)組時(shí)必須用整數(shù),那我們可以將處理函數(shù)的返回值設(shè)為整型,然后用這個(gè)返回值去初始化整型數(shù)組也是可以的:
void ShowList() { cout << endl; } //處理函數(shù) template<class T> int PrintArg(const T& t)//返回值為int類型 { cout << t << " "; return 0; } //展開函數(shù) template<class ...Args> void ShowList(Args... args) { int arr[] = { PrintArg(args)... }; //列表初始化 cout << endl; }
emplace
C++11 給 STL 容器增加 emplace 的插入接口,比如 list 容器的 push_front、push_back 和insert 函數(shù),都有了對應(yīng)的 emplace_front、emplace_back 和 emplace 函數(shù):
這些emplace版本的插入接口支持模板的可變參數(shù),比如list容器的emplace_back函數(shù)的聲明如下:
emplace 接口的可變模板參數(shù)類型都帶有KaTeX parse error: Expected '}', got '&' at position 14: \color{red} {&?&} ,這個(gè)表示的是萬能引用,而不是右值引用。
使用方法
emplace 接口使用方式與容器原有的插入接口使用方式類似,但又有一些不同之處,以 list 的 emplace_back 和 push_back 為例:
調(diào)用 push_back 插入元素時(shí),可以傳入左值對象或右值對象,也可以使用列表進(jìn)行初始化;調(diào)用emplace_back 插入元素時(shí),也可以傳入左值對象或右值對象,但不可以使用列表進(jìn)行初始化。
除此之外,emplace系列接口最大的特點(diǎn)就是,插入元素可傳入用于構(gòu)造元素的參數(shù)包
int main() { list<pair<int, string>> mylist; pair<int, string> kv(10, "111"); mylist.push_back(kv); //左值 mylist.push_back(pair<int, string>(20, "222")); //右值 mylist.push_back({ 30, "333" }); //列表初始化 mylist.emplace_back(kv); //左值 mylist.emplace_back(pair<int, string>(40, "444")); //右值 mylist.emplace_back(50, "555"); //參數(shù)包 return 0; }
工作原理
emplace 接口先通過空間配置器為新結(jié)點(diǎn)獲取一塊內(nèi)存空間,注意這里只會(huì)開辟空間,不會(huì)自動(dòng)調(diào)用構(gòu)造函數(shù)對這塊空間進(jìn)行初始化。
然后調(diào)用 allocator_traits::construct 函數(shù)對這塊空間進(jìn)行初始化,調(diào)用該函數(shù)會(huì)傳入這塊空間的地址和用戶傳入的參數(shù),注意要完美轉(zhuǎn)發(fā);在 allocator_traits::construct 中會(huì)使用定位 new 表達(dá)式,顯示調(diào)用構(gòu)造函數(shù)對這塊空間進(jìn)行初始化,調(diào)用構(gòu)造函數(shù)時(shí)會(huì)傳入用戶傳入的參數(shù),這里同樣需要完美轉(zhuǎn)發(fā)
最后將初始化好的新結(jié)點(diǎn)插入到對應(yīng)的數(shù)據(jù)結(jié)構(gòu)中,比如 list 就是將新結(jié)點(diǎn)插入到底層的雙鏈表中
意義
emplace 接口的可變參數(shù)模板類型都是萬能引用,因此既可以接收左值,也可以接收右值,還可以接收參數(shù)包
如果調(diào)用 emplace 接口時(shí)傳入的是左值,首先需要先在此之前調(diào)用構(gòu)造函數(shù)實(shí)例化出一個(gè)左值對象,最后使用定位 new 表達(dá)式調(diào)用構(gòu)造函數(shù)對空間進(jìn)行初始化時(shí),會(huì)匹配到拷貝構(gòu)造函數(shù)
如果調(diào)用 emplace 接口時(shí)傳入的是右值,那么就需要在此之前調(diào)用構(gòu)造函數(shù)實(shí)例化出一個(gè)右值對象,最終在使用定位new表達(dá)式調(diào)用構(gòu)造函數(shù)對空間進(jìn)行初始化時(shí),就會(huì)匹配到移動(dòng)構(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ù)
一句話就是:
傳入左值,調(diào)用構(gòu)造函數(shù)+拷貝構(gòu)造函數(shù)。
傳入右值,調(diào)用構(gòu)造函數(shù)+移動(dòng)構(gòu)造函數(shù)。
傳入?yún)?shù)包,只需要調(diào)用構(gòu)造函數(shù)
注意,這里前提是容器中存儲(chǔ)的是一個(gè)需要深拷貝的類,并且該類實(shí)現(xiàn)了移動(dòng)構(gòu)造函數(shù),否則傳入左值和傳入右值的效果是一樣的,都會(huì)調(diào)用一次構(gòu)造和一次拷貝構(gòu)造
因?yàn)槿萜髟械?push_back、push_front 和 insert 也提供了右值引用的接口,所以 emplace 的部分功能和原有容器是重復(fù)的,如果調(diào)用時(shí)傳入右值,那么最終也會(huì)調(diào)用對應(yīng)的移動(dòng)構(gòu)造函數(shù)進(jìn)行資源轉(zhuǎn)移。
emplace 最大特點(diǎn)就是支持傳入?yún)?shù)包,用這些參數(shù)包直接構(gòu)造出對象,這樣就能減少一次拷貝,這就是為什么有人說 emplace 系列接口更高效的原因
但 emplace 并不是在所有場景下都比原有的插入接口高效,如果傳入的是左值對象或右值對象,那么 emplace 系列接口的效率其實(shí)和原有的效率是一樣的
emplace 真正高效的情況是傳入?yún)?shù)包的時(shí)候, 直接通過參數(shù)包構(gòu)造出對象,避免了中途的一次拷貝 \color{red} {直接通過參數(shù)包構(gòu)造出對象,避免了中途的一次拷貝} 直接通過參數(shù)包構(gòu)造出對象,避免了中途的一次拷貝
namespace cl { class string { public: //構(gòu)造函數(shù) string(const char* str = "") { cout << "string(const char* str) -- 構(gòu)造函數(shù)" << endl; _size = strlen(str); _capacity = _size; _str = new char[_capacity + 1]; //開辟空間(多開一個(gè)用于存放'\0') strcpy(_str, str); //將C字符串拷貝到已開好的空間 } //交換兩個(gè)對象數(shù)據(jù) void swap(string& s) { std::swap(_str, s._str); //交換兩個(gè)對象的C字符串 std::swap(_size, s._size); //交換兩個(gè)對象的大小 std::swap(_capacity, s._capacity); //交換兩個(gè)對象的容量 } //拷貝構(gòu)造函數(shù)(現(xiàn)代寫法) string(const string& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(const string& s) -- 拷貝構(gòu)造" << endl; string tmp(s._str); //調(diào)用構(gòu)造函數(shù),構(gòu)造一個(gè)s._str的對象 swap(tmp); //交換這兩個(gè)對象 } //移動(dòng)構(gòu)造 string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- 移動(dòng)構(gòu)造" << endl; swap(s); } //拷貝賦值函數(shù)(現(xiàn)代寫法) string& operator=(const string& s) { cout << "string& operator=(const string& s) -- 深拷貝" << endl; string tmp(s); swap(tmp); //交換 return *this; //返回左值 } //移動(dòng)賦值 string& operator(string&& s) { cout << "string& operator=(string&& s) -- 移動(dòng)賦值" << endl; swap(s); return *this; } //析構(gòu)函數(shù) ~string() { //delete[] _str; //釋放_str指向的空間 _str = nullptr; //置空,防止非法訪問 _size = 0; _capacity = 0; } private: char* _str; size_t _size; size_t _capacity; }; }
這里我們用模擬實(shí)現(xiàn)的 string 來驗(yàn)證 emplace 的機(jī)制:
int main() { list<pair<int, cl::string>> mylist; pair<int, cl::string> kv(1, "one"); mylist.emplace_back(kv); //左值 cout << endl; mylist.emplace_back(pair<int, cl::string>(2, "two")); //右值 cout << endl; mylist.emplace_back(3, "three"); //參數(shù)包 return 0; }
結(jié)果如下:
我們自己實(shí)現(xiàn)的 string 的拷貝構(gòu)造函數(shù)復(fù)用了他的拷貝函數(shù),所以在調(diào)用 string 的拷貝構(gòu)造的時(shí)候會(huì)緊跟一次拷貝函數(shù)的調(diào)用。
當(dāng)然,如果想要更加完美的體現(xiàn) emplace 的作用,這里存的是 char 類型,為了體現(xiàn)參數(shù)包的概念,可以將 list 中更換成 pair 類型對象,這里不贅述了,有興趣的可自行實(shí)現(xiàn)。
總結(jié)
到此這篇關(guān)于C++可變參數(shù)模板的文章就介紹到這了,更多相關(guān)C++可變參數(shù)模板內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++和OpenCV實(shí)現(xiàn)圖像字符化效果
圖像字符化的意思是將圖像以字符形式呈現(xiàn),具有一定的娛樂價(jià)值,許多開發(fā)人員通過python實(shí)現(xiàn)該功能,C++實(shí)現(xiàn)的代碼較少,因此本文通過C++和OpenCV實(shí)現(xiàn),給予C++開發(fā)人員一些可供借鑒的思路,需要的朋友可以參考下2022-06-06C++實(shí)現(xiàn)獲取時(shí)間戳和計(jì)算運(yùn)行時(shí)長
這篇文章主要為大家詳細(xì)介紹了如何使用C++實(shí)現(xiàn)獲取時(shí)間戳和計(jì)算運(yùn)行時(shí)長功能,文中的示例代碼講解詳細(xì),有需要的小伙伴可以參考一下2024-12-12C語言實(shí)現(xiàn)的循環(huán)單鏈表功能示例
這篇文章主要介紹了C語言實(shí)現(xiàn)的循環(huán)單鏈表功能,結(jié)合實(shí)例形式分析了基于C語言實(shí)現(xiàn)的循環(huán)單鏈表定義、創(chuàng)建、添加、刪除、打印、排序等相關(guān)操作技巧,需要的朋友可以參考下2018-04-04QT設(shè)計(jì)秒表功能(跑步計(jì)時(shí)器)
這篇文章主要為大家詳細(xì)介紹了QT設(shè)計(jì)秒表功能,跑步計(jì)時(shí)器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08QT通過C++線程池運(yùn)行Lambda自定義函數(shù)流程詳解
最近在接觸公司的一個(gè)QT桌面項(xiàng)目,其中里面有一個(gè)模塊是使用線程池去運(yùn)行自定義函數(shù)的,自己潛心研究那個(gè)線程池代碼一天,發(fā)現(xiàn)研究不透,看不懂,里面幾乎都是使用C++11的新特性進(jìn)行編寫2022-10-10