一篇文章讓你徹底明白c++11增加的變參數(shù)模板
前言
本篇文章介紹一下c++11中增加的變參數(shù)模板template<typename... _Args>到底是咋回事,以及它的具體用法。
說(shuō)明一下,我用的是gcc7.1.0編譯器,標(biāo)準(zhǔn)庫(kù)源代碼也是這個(gè)版本的。
按照慣例,還是先看一下本文大綱,如下:
在之前寫(xiě)vector和deque容器源碼剖析的過(guò)程中,經(jīng)常發(fā)現(xiàn)這樣的代碼,如下:
template<typename... _Args> void emplace_front(_Args&&... __args);
可以看到里面模板參數(shù)是template<typename... _Args>,其實(shí)這個(gè)就是變參數(shù)模板,然后它的參數(shù)也是比較特別的_Args&&... __args,去除右值引用的話,它就是一個(gè)可變參數(shù),那么可變參數(shù)模板和可變參數(shù)到底是什么,應(yīng)該怎么使用呢,我們今天就來(lái)深究一下這些事情。
1. 什么是變參數(shù)模板
c++11中新增加了一項(xiàng)內(nèi)容,叫做變參數(shù)模板,所謂變參數(shù)模板,顧名思義就是參數(shù)個(gè)數(shù)和類型都可能發(fā)生變化的模板,要實(shí)現(xiàn)這一點(diǎn),那就必須要使用模板形參包。
模板形參包是可以接受0個(gè)或者n個(gè)模板實(shí)參的模板形參,至少有一個(gè)模板形參包的模板就可以稱作變參數(shù)模板,所以說(shuō)白了,搞懂了模板形參包就明白變參數(shù)模板了,因?yàn)樽儏?shù)模板就是基于模板形參包來(lái)實(shí)現(xiàn)的,接下來(lái)我們就來(lái)看看到底啥是模板形參包。
2. 變參數(shù)模板的基礎(chǔ)-模板形參包
模板形參包主要出現(xiàn)在函數(shù)模板和類模板中,目前來(lái)講,模板形參包主要有三種,即:非類型模板形參包、類型模板形參包、模板模板形參包。
2.1 非類型模板形參包
非類型模板形參包語(yǔ)法是這樣的:
template<類型 ... args>
初看會(huì)很疑惑,說(shuō)是非類型模板形參包,怎么語(yǔ)法里面一開(kāi)始就是一個(gè)類型的,其實(shí)這里的非類型是針對(duì)typename和class關(guān)鍵字來(lái)的,都知道模板使用typename或者class關(guān)鍵字表示它們后面跟著的名稱是類型名稱,而這里的形參包里面類型其實(shí)表示一個(gè)固定的類型,所以這里其實(shí)不如叫做固定類型模板形參包。
對(duì)于上述非類型模板形參包而言,類型選擇一個(gè)固定的類型,args其實(shí)是一個(gè)可修改的參數(shù)名,如下:
template<int ... data> xxxxxx;
注意,這個(gè)固定的類型是有限制的,標(biāo)準(zhǔn)c++規(guī)定,只能為整型、指針和引用。
但是這個(gè)形參包該怎么用呢,有這樣一個(gè)例子,比如我想統(tǒng)計(jì)這個(gè)幼兒園的小朋友們的年齡總和,但是目前并不知道總共有多少個(gè)小朋友,那么此時(shí)就可以用這個(gè)非類型模板形參包,代碼如下:
#include <iostream> using namespace std; //這里加一個(gè)空模板函數(shù)是為了編譯可以通過(guò),否則編譯期間調(diào)用printAmt<int>(int&)就會(huì)找不到可匹配的函數(shù) //模板參數(shù)第一個(gè)類型實(shí)際上是用不到的,但是這里必須要加上,否則就是調(diào)用printAmt<>(int&),模板實(shí)參為空,但是模板形參列表是不能為空的 template<class type> void printAmt(int &iSumAge) { return; } template<class type, int age0, int ... age> void printAmt(int &iSumAge) { iSumAge += age0; //這里sizeof ... (age)是計(jì)算形參包里的形參個(gè)數(shù),返回類型是std::size_t,后續(xù)同理 if ( (sizeof ... (age)) > 0 ) { //這里的age...其實(shí)就是語(yǔ)法中的一種包展開(kāi),這個(gè)后續(xù)會(huì)具體說(shuō)明 printAmt<type, age...>(iSumAge); } } int main() { int sumAge = 0; printAmt<int,1,2,3,4,5,7,6,8>(sumAge); cout << "the sum of age is " << sumAge << endl; return 0; }
這里只是以此為例來(lái)說(shuō)明一下非類型模板形參包的使用,實(shí)際項(xiàng)目中這么簡(jiǎn)單的事肯定是沒(méi)有必要還寫(xiě)個(gè)模板的。
根據(jù)語(yǔ)法和代碼的使用情況,我們對(duì)非類型模板形參包總結(jié)如下:
- 非類型模板形參包類型是固定的,但參數(shù)名跟普通函數(shù)參數(shù)一樣,是可以修改的;
- 傳遞給非類型模板形參包的實(shí)參不是類型,而是實(shí)際的值。
2.2 類型模板形參包
類型模板形參包語(yǔ)法如下:
typename|class ... Args
這個(gè)就是很正常的模板形參了哈,typename關(guān)鍵字和class關(guān)鍵字都可以用于在模板中聲明一個(gè)未知類型,只是在以前template<typename type>的基礎(chǔ)上加了一個(gè)省略號(hào),改成了可變形參包而已,該可變形參包可以接受無(wú)限個(gè)不同的實(shí)參類型。
現(xiàn)在我們先用一下這個(gè)類型模板形參包看看,假設(shè)我們有這樣一種場(chǎng)景,我想輸出一個(gè)人的姓名、性別、年齡、身高等個(gè)人信息,但是具體有哪些信息我們不能確定,那應(yīng)該怎么辦呢?
分析一下,具體信息不固定,類型也不固定,此時(shí)就可以使用類型模板形參包了,看下面這段代碼:
#include <iostream> using std::cout; using std::endl; void xprintf() { cout << endl; } template<typename T, typename... Targs> void xprintf(T value, Targs... Fargs) { cout << value << ' '; if ( (sizeof ...(Fargs)) > 0 ) { //這里調(diào)用的時(shí)候沒(méi)有顯式指定模板,是因?yàn)楹瘮?shù)模板可以根據(jù)函數(shù)參數(shù)自動(dòng)推導(dǎo) xprintf(Fargs...); } else { xprintf(); } } int main() { xprintf("小明個(gè)人信息:", "小明", "男", 35, "程序員", 169.5); return 0; }
輸出結(jié)果如下:
小明個(gè)人信息: 小明 男 35 程序員 169.5
這個(gè)就是一個(gè)類型模板形參包在函數(shù)模板里面的典型使用,可以看到,
當(dāng)然啦,有人會(huì)說(shuō)了,其實(shí)cout一行代碼就可以搞定,但是我們這里是提供通用型接口,具體要輸出哪些信息事先并不知道,這個(gè)時(shí)候使用類型模板形參包就很方便啦。
2.3 模板模板形參包
這個(gè)就有點(diǎn)繞了,模板模板形參包,有點(diǎn)不好理解,還是先看一下語(yǔ)法看看:
template < 形參列表 > class ... Args(可選)
其實(shí)說(shuō)白了,就是說(shuō)這個(gè)形參包本身它也是一個(gè)模板,在看模板模板形參包之前,我們先介紹一下模板模板形參,因?yàn)樾螀f(shuō)白了,就是在形參的基礎(chǔ)上增加了省略號(hào)實(shí)現(xiàn)的。
我們先看一下標(biāo)準(zhǔn)庫(kù)中對(duì)模板模板形參的使用,找到頭文件bits/alloc_traits.h,在模板類allocator_traits的聲明中有這樣一個(gè)結(jié)構(gòu)體,如下:
template<template<typename> class _Func, typename _Tp> struct _Ptr<_Func, _Tp, __void_t<_Func<_Alloc>>> { using type = _Func<_Alloc>; };
這里的意思就是說(shuō)_Func這個(gè)模板形參本身是一個(gè)帶模板的類型,使用的時(shí)候是需要聲明模板實(shí)參的。
假設(shè)有這樣一種場(chǎng)景,我們需要定義一個(gè)vector變量,但不能確定vector的元素類型,此時(shí)該怎么辦呢?
看如下代碼:
#include <typeinfo> #include <cxxabi.h> #include <iostream> #include <vector> //將gcc編譯出來(lái)的類型翻譯為真實(shí)的類型 const char* GetRealType(const char* p_szSingleType) { const char* szRealType = abi::__cxa_demangle(p_szSingleType, nullptr, nullptr, nullptr); return szRealType; } //這里的func是一個(gè)模板模板形參 template<template<typename, typename> class func, typename tp, typename alloc = std::allocator<tp> > struct temp_traits { using type = func<tp, alloc>; type tt;//根據(jù)模板類型定義一個(gè)成員變量 }; int main() { temp_traits<std::vector, int> _traits; //獲取結(jié)構(gòu)體字段tt的類型 const std::type_info &info = typeid(_traits.tt); std::cout << GetRealType(info.name()) << std::endl; return 0; }
輸出結(jié)果如下:
std::vector<int, std::allocator<int> >
這里類型temp_tratis里面根據(jù)模板模板形參和其他模板形參來(lái)實(shí)現(xiàn)了我們的使用場(chǎng)景。
理解了模板模板形參,再來(lái)看看模板模板形參包的使用,這個(gè)與類型模板形參包沒(méi)什么兩樣,只不過(guò)類型換成了一個(gè)帶模板的類型而已,看下面這段代碼:
#include <typeinfo> #include <cxxabi.h> #include <iostream> #include <vector> #include <deque> #include <list> //將gcc編譯出來(lái)的類型翻譯為真實(shí)的類型 const char* GetRealType(const char* p_szSingleType) { const char* szRealType = abi::__cxa_demangle(p_szSingleType, nullptr, nullptr, nullptr); return szRealType; } //泛化變參模板 template<typename tp, typename alloc, template<typename, typename> class ... types > struct temp_traits { temp_traits(tp _tp) { std::cout << "泛化模板執(zhí)行" << std::endl; } }; //偏特化變參模板 template< typename tp, typename alloc, template<typename, typename> class type, template<typename, typename> class ... types > struct temp_traits<tp, alloc,type, types...>:public temp_traits<tp, alloc, types...> { using end_type = type<tp, alloc>; end_type m_object; temp_traits(tp _tp) :temp_traits<tp, alloc, types...>(_tp) { const std::type_info &info = typeid(m_object); std::cout << "偏特化版本執(zhí)行, 此時(shí)類型:" << GetRealType(info.name()) << std::endl; m_object.push_back(_tp); } void print() { auto it = m_object.begin(); for(;it != m_object.end(); ++it) { std::cout << "類型為:" << GetRealType(typeid(end_type).name()) << ", 數(shù)據(jù)為:" << *it << std::endl; } } }; int main() { temp_traits<int, std::allocator<int>, std::vector, std::deque, std::list> _traits(100); _traits.print(); return 0; }
這段代碼就相當(dāng)不好理解了,我們可以認(rèn)為它是一個(gè)遞歸繼承的過(guò)程,但到底是怎么個(gè)遞歸繼承法呢?可以先看一下執(zhí)行結(jié)果,由結(jié)果來(lái)倒推遞歸過(guò)程。
先看一下執(zhí)行結(jié)果,如下:
泛化模板執(zhí)行
偏特化版本執(zhí)行, 此時(shí)類型:std::__cxx11::list<int, std::allocator<int> >
偏特化版本執(zhí)行, 此時(shí)類型:std::deque<int, std::allocator<int> >
偏特化版本執(zhí)行, 此時(shí)類型:std::vector<int, std::allocator<int> >
類型為:std::vector<int, std::allocator<int> >, 數(shù)據(jù)為:100
根據(jù)4次構(gòu)造函數(shù)的調(diào)用,我們可以得出結(jié)論:形參包包含多少個(gè)形參,它就會(huì)在此基礎(chǔ)上有幾層繼承,所以現(xiàn)在是3個(gè)形參,3層繼承,頂層基類是泛化模板,然后進(jìn)行了三層派生,這個(gè)遞歸繼承的過(guò)程是編譯器根據(jù)代碼自行展開(kāi)的。
再看看對(duì)于成員函數(shù)print的調(diào)用,我的原意是想針對(duì)每一種容器類型,都打印出結(jié)果,但現(xiàn)在只打印了一種,我們可以想想,對(duì)于繼承,非虛函數(shù)但函數(shù)類型相同的情況下,派生類的成員函數(shù)會(huì)覆蓋基類的成員函數(shù),所以這里結(jié)果是正常的。
那么怎么實(shí)現(xiàn)我們要的效果呢,答案是使用析構(gòu)函數(shù),層層析構(gòu),所以將成員函數(shù)print函數(shù)修改為如下代碼:
~temp_traits() { auto it = m_object.begin(); for(;it != m_object.end(); ++it) { std::cout << "類型為:" << GetRealType(typeid(end_type).name()) << ", 數(shù)據(jù)為:" << *it << std::endl; } }
此時(shí)輸出結(jié)果如下:
泛化模板執(zhí)行
偏特化版本執(zhí)行, 此時(shí)類型:std::__cxx11::list<int, std::allocator<int> >
偏特化版本執(zhí)行, 此時(shí)類型:std::deque<int, std::allocator<int> >
偏特化版本執(zhí)行, 此時(shí)類型:std::vector<int, std::allocator<int> >
類型為:std::vector<int, std::allocator<int> >, 數(shù)據(jù)為:100
類型為:std::deque<int, std::allocator<int> >, 數(shù)據(jù)為:100
類型為:std::__cxx11::list<int, std::allocator<int> >, 數(shù)據(jù)為:100
到這里,我們對(duì)模板模板形參包應(yīng)該就有了比較深的了解了。
注意,不論是哪種形參包,形參包都需要放在模板的最后面,否則編譯就會(huì)有問(wèn)題。
3. 模板形參包的延伸-函數(shù)形參包
我們都知道函數(shù)形參是什么,那么函數(shù)形參包呢,它到底是什么,先看看函數(shù)形參包的語(yǔ)法:
Args ... args
這里的Args...代表形參包類型,這個(gè)類型就是模板形參包里面聲明的類型,args就是函數(shù)的形參名稱了,是可以自定義的。
那么是所有的模板形參包聲明類型都可以作為函數(shù)形參包類型嗎,不是的,前面我們講了三種模板形參包,這其中除了非類型的模板形參包因?yàn)轭愋凸潭ㄇ沂蔷唧w的值,不能作為函數(shù)形參包以外,類型模板形參包和模板模板形參包因?yàn)槁暶鞯亩际穷愋?,所以他們是可以用作函?shù)形參的類型的。
類型模板形參包聲明函數(shù)形參我們?cè)?.2節(jié)的代碼舉例里面已經(jīng)說(shuō)明了,這里不再舉例,我們看下模板模板行參包怎么樣作為函數(shù)的形參,代碼如下:
#include <typeinfo> #include <cxxabi.h> #include <iostream> #include <vector> #include <list> #include <deque> //將gcc編譯出來(lái)的類型翻譯為真實(shí)的類型 const char* GetRealType(const char* p_szSingleType) { const char* szRealType = abi::__cxa_demangle(p_szSingleType, nullptr, nullptr, nullptr); return szRealType; } void xprintf() { std::cout << "調(diào)用空函數(shù)" << std::endl; } template<typename tp, typename alloc, template<typename, typename> class T, template<typename, typename> class ... Targs > void xprintf(T<tp, alloc> value, Targs<tp, alloc>... Fargs) { std::cout << "容器類型:" << GetRealType(typeid(value).name()) << std::endl; std::cout << "容器數(shù)據(jù):" << std::endl; auto it = value.begin(); for(; it != value.end(); ++it) { std::cout << *it << ','; } std::cout << std::endl; if ( (sizeof ...(Fargs)) > 0 ) { //這里調(diào)用的時(shí)候沒(méi)有顯式指定模板,是因?yàn)楹瘮?shù)模板可以根據(jù)函數(shù)參數(shù)自動(dòng)推導(dǎo) xprintf(Fargs...); } else { xprintf(); } } int main() { std::vector<int> vt; std::deque<int> dq; std::list<int> ls; for(int i =0 ; i < 10 ; ++i) { vt.push_back(i); dq.push_back(i); ls.push_back(i); } xprintf(vt, dq, ls); return 0; }
這個(gè)就是一個(gè)典型的使用模板模板形參包類型作為函數(shù)形參的案例,說(shuō)白了,我們要理解函數(shù)形參包的本質(zhì),它其實(shí)還是一個(gè)函數(shù)形參,既然是函數(shù)形參,就脫離不了類型加參數(shù)名的語(yǔ)法,形參包無(wú)非就是在類型后面加個(gè)省略號(hào),而模板模板形參包作為函數(shù)形參類型的時(shí)候一定要記得加模板參數(shù),比如代碼里面T<tp, alloc>這樣才是一個(gè)完整的類型,光是一個(gè)T,它的類型就是不完整的。
理解了以上的這一點(diǎn),我們對(duì)函數(shù)形參包的使用就沒(méi)有難度了。
4. 模板形參包的展開(kāi)方法
到底啥是形參包展開(kāi),我們先看看語(yǔ)法,如下:
模式 ...
在模式后面加省略號(hào),就是包展開(kāi)了,而所謂的模式一般都是形參包名稱或者形參包的引用,包展開(kāi)以后就變成零個(gè)或者多個(gè)逗號(hào)分隔的實(shí)參。
比如上面的age ...和Fargs...都屬于包展開(kāi),但是要知道,這種形式我們是沒(méi)有辦法直接使用的,那么具體該怎么使用呢,有兩種辦法:
- 一是使用遞歸的辦法把形參包里面的參數(shù)一個(gè)一個(gè)的拿出來(lái)進(jìn)行處理,最后以一個(gè)默認(rèn)的函數(shù)或者特化模板類來(lái)結(jié)束遞歸;
- 二是直接把整個(gè)形參包展開(kāi)以后傳遞給某個(gè)適合的函數(shù)或者類型。
遞歸方法適用場(chǎng)景:多個(gè)不同類型和數(shù)量的參數(shù)有比較相似的動(dòng)作的時(shí)候,比較適合使用遞歸的辦法。
關(guān)于遞歸辦法的使用,前面幾節(jié)有多個(gè)案例了,這里不再展開(kāi)多說(shuō)。
關(guān)于整個(gè)形參包傳遞的使用方法,看下面代碼:
#include <iostream> #include <string> using namespace std; class programmer { string name; string sex; int age; string vocation;//職業(yè) double height; public: programmer(string name, string sex, int age, string vocation, double height) :name(name), sex(sex), age(age), vocation(vocation), height(height) { cout << "call programmer" << endl; } void print() { cout << "name:" << name << endl; cout << "sex:" << sex << endl; cout << "age:" << age << endl; cout << "vocation:" << vocation << endl; cout << "height:" << height << endl; } }; template<typename T> class xprintf { T * t; public: xprintf() :t(nullptr) {} template<typename ... Args> void alloc(Args ... args) { t = new T(args...); } void print() { t->print(); } void afree() { if ( t != nullptr ) { delete t; t = nullptr; } } }; int main() { xprintf<programmer> xp; xp.alloc("小明", "男", 35, "程序員", 169.5); xp.print(); xp.afree(); return 0; }
這里類型xprintf是一個(gè)通用接口,類模板中類型T是一個(gè)未知類型,我們不知道它的構(gòu)造需要哪些類型、多少個(gè)參數(shù),所以這里就可以在它的成員函數(shù)中使用變參數(shù)模板,來(lái)直接把整個(gè)形參包傳遞給構(gòu)造函數(shù),具體需要哪些實(shí)參就根據(jù)模板類型T的實(shí)參類型來(lái)決定。
5. stl中使用模板形參包的案例
再來(lái)說(shuō)回一開(kāi)始的案例,如下:
template<typename... _Args> void emplace_front(_Args&&... __args);
這個(gè)是deque容器里面的函數(shù),函數(shù)emplace_front可以說(shuō)是push_front的一個(gè)優(yōu)化版本,從它的原型可以看出,這個(gè)函數(shù)就是類型模板形參包的典型使用,只不過(guò)這里多了兩個(gè)符號(hào)&&,這個(gè)我們先前也講過(guò),它代表右值引用,對(duì)于右值引用,如果元素類型是int、double這樣的原生類型,其實(shí)右值引用和直接傳值,區(qū)別不是很大。
那么這里函數(shù)原型中的參數(shù)_Args&&... __args到底代表什么呢,拋開(kāi)右值引用不說(shuō),它就是多個(gè)參數(shù),難道是可以在容器中插入多個(gè)不同類型的元素嗎,并不是啊,容器中的元素是必須要一致的,這里的參數(shù)其實(shí)是容器定義時(shí)元素類型構(gòu)造函數(shù)的多個(gè)參數(shù),也就是說(shuō),函數(shù)emplace_front可以直接傳入元素的構(gòu)造參數(shù),下面我們看看到底是怎么使用的,代碼如下:
#include <deque> #include <string> #include <iostream> class CMan { int age; std::string sex; double money; public: CMan(int age, std::string sex, double money) :age(age), sex(sex), money(money) { std::cout << "call contrust" << std::endl; } CMan(CMan && other) :age(other.age), sex(other.sex), money(other.money) { std::cout << "call move contrust" << std::endl; } }; int main() { std::deque<CMan> dq; dq.emplace_front(30, "man", 12.3); return 0; }
可以看到,它就是利用了變參數(shù)模板的特性,傳入了多個(gè)不同的構(gòu)造入?yún)?,那么這些構(gòu)造入?yún)⑹窃趺磦魅氲筋怌Man本身的呢,我們看看函數(shù)emplace_front的源碼實(shí)現(xiàn),如下:
#if __cplusplus >= 201103L template<typename _Tp, typename _Alloc> template<typename... _Args> #if __cplusplus > 201402L typename deque<_Tp, _Alloc>::reference #else void #endif deque<_Tp, _Alloc>:: emplace_front(_Args&&... __args) { if (this->_M_impl._M_start._M_cur != this->_M_impl._M_start._M_first) { _Alloc_traits::construct(this->_M_impl, this->_M_impl._M_start._M_cur - 1, std::forward<_Args>(__args)...); --this->_M_impl._M_start._M_cur; } else _M_push_front_aux(std::forward<_Args>(__args)...); #if __cplusplus > 201402L return front(); #endif }
可以看到,實(shí)際上是使用了std::forward來(lái)把形參包整個(gè)傳遞到內(nèi)存分配器里面去,然后在內(nèi)存分配器里面又通過(guò)調(diào)用operator new和std::forward把形參包傳遞給了容器的元素類型的構(gòu)造函數(shù)。
std::forward意思是完美轉(zhuǎn)發(fā),可以把參數(shù)原封不動(dòng)的傳遞下去。
這么一看,這不就是我們第4節(jié)里面說(shuō)的形參包展開(kāi)的第二種方法的一種實(shí)際使用案例嗎,只是這里使用了std::forward實(shí)現(xiàn)了完美轉(zhuǎn)發(fā)而已。
總結(jié)
到此這篇關(guān)于c++11變參數(shù)模板的文章就介紹到這了,更多相關(guān)c++11變參數(shù)模板內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vc中float與DWORD的互想轉(zhuǎn)換實(shí)現(xiàn)代碼
這篇文章主要介紹了vc中float與DWORD的互想轉(zhuǎn)換實(shí)現(xiàn)代碼,需要的朋友可以參考下2017-06-06

C++ 智能指針使用不當(dāng)導(dǎo)致內(nèi)存泄漏問(wèn)題解析

有關(guān)C++中隨機(jī)函數(shù)rand() 和srand() 的用法詳解

C++實(shí)現(xiàn)接兩個(gè)鏈表實(shí)例代碼

C語(yǔ)言如何求整數(shù)的位數(shù)及各位數(shù)字之和

QT連接SQLServer數(shù)據(jù)庫(kù)的實(shí)現(xiàn)