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

有關C++中隨機函數rand() 和srand() 的用法詳解