
概述
首先這篇文章出自博客園作者:[ ?? qicosmos ],我對本文的實體代碼進行了學習、思考和整理糾正,理清了文章的全部細節,覺得這是一篇讓我受益匪淺的文章,之所以會接觸「可變引數模板」這部分的內容,是因為我當下剛好在學C++11 function機制,其內部實作需要接收不定長度的引數,因此需要用到「可變引數模板」相關的知識,本文有很多的C++模板元編程「黑魔法」是我之前從來沒接觸過的,比如模板遞回展開、型別萃取type_traits中的基石integral_constant等等,C++的學習之路任重而道遠呀,那廢話不多說,我們來說說今天的主題,C++11的可變引數模板,
C++11的新特性「可變引數模板(variadic templates)」是C++11新增的「最強大」的特性之一,它對引數進行了高度泛化,它能表示0到任意個數、任意型別的引數,相比C++98/03,類模板和函式模板中只能含固定數量的模板引數,可變模板引數無疑是一個巨大的改進,然而由于可變模板引數比較抽象,使用起來需要一定的技巧,所以它也是C++11中最難理解和掌握的特性之一,雖然掌握可變引數模板有一定難度,但是它卻是C++11 中最有意思的一個特性,本文希望帶領讀者由淺入深的認識和掌握這一特性,同時也會通過一些實體來展示可變引數模板的一些用法,
可變模板的引數展開
可變引數模板和普通模板的語意是一樣的,只是寫法上稍有區別,宣告可變引數模板時需要在typename或class后面帶上省略號「...」,比如我們常常這樣宣告一個可變模板引數:template<typename...>或者template<class...>,一個典型的可變模板引數的定義是這樣的:
template <typename... T>
void f(T... args);
上面的可變模板引數的定義當中,省略號的作用有兩個:
- 宣告一個引數包
T... args,這個引數包中可以包含0到任意個模板引數; - 在模板定義的右邊,可以將引數包展開成一個一個獨立的引數,
上面的引數args「前面」有省略號,所以它就是一個可變模板引數,我們把帶省略號的引數稱為“引數包”,它里面包含了0到N(N>=0)個模板引數,我們無法直接獲取引數包args中的每個引數的,只能通過「展開引數包」的方式來獲取引數包中的每個引數,這是使用可變模板引數的一個「主要特點」,也是「最大的難點」,即如何展開可變模板引數,
可變模板引數和普通的模板引數語意是一致的,所以可以應用于函式和類,即「可變引數模板函式」和「可變引數模板類」,然而,模板函式不支持偏特化,所以可變引數模板函式和可變引數模板類展開可變引數的方法還不盡相同,下面我們來分別看看他們展開可變引數的方法,
可變引數函式模板
一個簡單的可變引數函式模板:
#include <iostream>
using namespace std;
template<typename... T>
void f(T... args) {
cout << sizeof...(args) << endl;
}
int main() {
f(); // 0
f(1, 2); // 2
f(1, 2, "");// 3
return 0;
}
上面的例子中,f()沒有傳入引數,所以引數包為空,輸出的size為0,后面兩次呼叫分別傳入兩個和三個引數,故輸出的size分別為2和3,由于可變模板引數的型別和個數是不固定的,所以我們可以傳任意型別和個數的引數給函式f,這個例子只是簡單的將可變引數模板的個數列印出來,如果我們需要將引數包中的每個引數列印出來的話就需要通過一些方法了,展開可變模板引數函式的方法一般有兩種:
- 一種是通過「遞回函式」來展開引數包
- 另外一種是通過「逗號運算式」來展開引數包
下面來看看如何用這兩種方法來展開引數包,
遞回函式方式展開引數包
通過遞回函式展開引數包,需要提供一個「引數包展開的函式」和一個「遞回終止函式」,遞回終止函式正是用來終止遞回的,來看看下面的例子,
#include <iostream>
using namespace std;
// @note 遞回終止函式
void print() {
cout << "empty" << endl;
}
// @note 展開函式
template<typename T, typename... Args>
void print(T head, Args... rest) {
cout << "parameter " << head << endl;
print(rest...);
}
int main() {
print(1, 2, 3, 4);
return 0;
}
上例會輸出每一個引數,直到為空時輸出empty,展開引數包的函式有兩個,一個是遞回函式,另外一個是遞回終止函式,引數包Args...在展開的程序中遞回呼叫自己,每呼叫一次引數包中的引數就會少一個,直到所有的引數都展開為止,當沒有引數時,則呼叫非模板函式print終止遞回程序,
遞回呼叫的程序是這樣的:
print(1,2,3,4);
print(2,3,4);
print(3,4);
print(4);
print();
上面的遞回終止函式還可以寫成這樣:
template<typename T>
void print(T t) {
cout << t << endl;
}
修改遞回終止函式后,上例中的呼叫程序是這樣的:
print(1, 2, 3, 4);
print(2, 3, 4);
print(3, 4);
print(4);
當引數包展開到最后一個引數時遞回終止,
再看一個通過可變模板引數求和的例子:
#include <iostream>
using namespace std;
template<typename T>
T sum(T t) {
return t;
}
template<typename T, typename... Types>
T sum(T first, Types... rest) {
return first + sum<T>(rest...);
}
int main() {
cout << sum(1, 2, 3, 4) << endl; // 10
return 0;
}
sum在展開引數包的程序中將各個引數相加求和,引數的展開方式和前面的列印引數包的方式是一樣的,
逗號運算式展開引數包
遞回函式展開引數包是一種標準做法,也比較好理解,但也有一個缺點,就是「必須」要一個多載的遞回終止函式,即「必須」要有一個同名的終止函式來終止遞回,這樣可能會感覺稍有不便,有沒有一種更簡單的方式呢?其實還有一種方法可以不通過遞回方式來展開引數包,這種方式需要借助「逗號運算式」和「初始化串列」,比如前面print的例子可以改成這樣:
#include <iostream>
using namespace std;
template<typename T>
void printArg(T t) {
cout << t << endl;
}
template<typename... Args>
void expand(Args... args) {
int arr[] = {(printArg(args), 0)...};
}
int main() {
expand(1, 2, 3, 4);
return 0;
}
這個例子將分別列印出1、2、3、4四個數字,這種展開引數包的方式,「不需要」通過遞回終止函式,是直接在expand函式體中展開的,printArg不是一個遞回終止函式,只是一個處理引數包中每一個引數的函式,這種就地展開引數包的方式實作的關鍵是逗號運算式,我們知道逗號運算式會按順序執行逗號前面的運算式,比如:
d = (a = b, c);
這個運算式會按順序執行:b會先賦值給a,接著括號中的逗號運算式回傳c的值,因此d將等于c,
expand 函式中的逗號運算式:
(printArg(args), 0)
也是按照這個執行順序,先執行printArg(args),再得到逗號運算式的結果0,同時還用到了C++11的另外一個特性:「初始化串列」,通過初始化串列來初始化一個「變長陣列」:
{(printArg(args), 0)...}
將會展開成
{((printArg(arg1), 0), (printArg(arg2), 0), (printArg(arg3), 0), etc...)}
最侄訓創建一個元素值都為0的陣列int arr[sizeof...(Args)],由于是逗號運算式,在創建陣列的程序中會先執行逗號運算式前面的部分printArg(args)列印出引數,也就是說在構造int陣列的程序中就將引數包展開了,這個陣列的目的純粹是為了在陣列構造的程序展開引數包,我們可以把上面的例子再進一步改進一下,將函式作為引數,就可以支持lambda運算式了,從而可以少寫一個遞回終止函式了,具體代碼如下:
#include <iostream>
#include <functional>
using namespace std;
template<typename T, typename ...Args>
void expand(const T &func, Args&&... args) {
// 這里用到了完美轉發
int arr[] = { (func(std::forward<Args>(args)), 0)... };
// initializer_list<int>{ (func(std::forward<Args>(args)), 0)... };
}
int main() {
expand([](int i)->void{cout << i << endl;}, 1, 2, 3);
return 0;
}
其實上面這里的
T型別就是function<void int>型別,引數可以寫成const function<void(int)> &func,也可寫成function<void(int)> func,這都無妨,我們只需要知道這是參考、或者是使用了function機制即可,
上面的例子將列印出每個引數,這里如果再使用「C++14的新特性」泛型lambda運算式的話,可以寫更泛化的lambda運算式了(把引數改為auto):
expand([](auto i)->void{cout << i << endl;}, 1, 2.2, "hello");
可變引數類模板
可變引數模板類是一個帶可變引數的模板類,比如C++11中的元組std::tuple就是一個可變模板類,它的定義如下:
template<typename... Types>
class tuple;
這個可變引數模板類可以攜帶任意型別任意個數的模板引數:
std::tuple<int> tp1 = std::make_tuple(1);
std::tuple<int, double> tp2 = std::make_tuple(1, 2.5);
std::tuple<int, double, string> tp3 = std::make_tuple(1, 2.5, "");
//std::tuple<int, double, string> tp3 = {1, 2.5, ""};
//std::tuple<int, double, string> tp3(1, 2.5, "");
可變引數模板類的引數個數可以為0個,所以下面的定義也是也是合法的:
std::tuple<> tp;
可變引數模板類的引數包展開的方式和可變引數模板函式的展開方式不同,可變引數模板類的引數包展開需要通過「模板特化」和「繼承方式」去展開,展開方式比可變引數模板函式要復雜,下面我們來看一下展開可變引數模板類中的引數包的方法,
模板偏特化和遞回方式來展開引數包
可變引數模板類的展開一般需要定義兩到三個類,包括「類宣告」和「偏特化」的模板類,如下方式定義了一個基本的可變引數模板類:
#include <iostream>
using namespace std;
// 前向宣告
template<typename... Args>
struct Sum;
// 基本定義
template<typename First, typename... Rest>
struct Sum<First, Rest...> {
enum {
value = https://www.cnblogs.com/S1mpleBug/archive/2022/10/27/Sum::value + Sum::value
};
};
// 遞回終止
template
struct Sum {
enum {
value = sizeof(Last)
};
};
int main() {
cout << Sum::value << endl; // 14
// Sum s;
// cout << s.value << endl; // 4+8+2=14
return 0;
}
這個Sum類的作用是在編譯期計算出引數包中引數型別的size之和,通過Sum<int, double, short>::value就可以獲取這3個型別的size之和為14,這是一個簡單的通過可變引數模板類計算的例子,可以看到一個基本的可變引數模板應用類由三部分組成:
「第一部分」是:
template<typename... Args>
struct Sum;
它是前向宣告,宣告這個Sum類是一個可變引數模板類;
「第二部分」是類的定義:
template<typename First, typename... Rest>
struct Sum<First, Rest...> {
enum {
value = https://www.cnblogs.com/S1mpleBug/archive/2022/10/27/Sum::value + Sum::value
};
};
它定義了一個部分展開的可變引數模板類,告訴編譯器如何遞回展開引數包,
「第三部分」是特化的遞回終止類:
template<typename Last>
struct Sum<Last> {
enum {
value = https://www.cnblogs.com/S1mpleBug/archive/2022/10/27/sizeof(Last)
};
};
通過這個特化的類來終止遞回,
template<typename First, typename... Args>
struct Sum<First, Rest...> {
...
};
這個前向宣告要求Sum的模板引數至少有一個,因為可變引數模板中的模板引數可以有0個,有時候0個模板引數沒有意義,就可以通過上面的宣告方式來限定模板引數不能為0個,上面的這種三段式的定義也可以改為兩段式的,可以將前向宣告去掉,這樣定義:
#include <iostream>
using namespace std;
// 基本模板類定義
template<typename First, typename... Rest>
struct Sum {
enum {
value = https://www.cnblogs.com/S1mpleBug/archive/2022/10/27/Sum::value + Sum::value
};
};
// 特化的終止函式
template
struct Sum {
enum {
value = sizeof(Last)
};
};
int main() {
cout << Sum::value << endl; // 14
// Sum s;
// cout << s.value << endl; // 4+8+2=14
return 0;
}
上面的方式「只要」一個基本的「模板類定義」和一個「特化的終止函式」就行了,而且限定了模板引數至少有一個,
遞回終止模板類可以有「多種寫法」,比如上例的遞回終止模板類還可以這樣寫:
#include <iostream>
using namespace std;
template<typename... Args>
struct Sum;
// 基本模板類定義
template<typename First, typename... Rest>
struct Sum<First, Rest...> {
enum {
value = https://www.cnblogs.com/S1mpleBug/archive/2022/10/27/sizeof(First) + Sum::value
};
};
// 遞回終止模板類
template
struct Sum {
enum {
value = sizeof(First) + sizeof(Last)
};
};
int main() {
// cout << Sum<>::value << endl; // error
// cout << Sum::value << endl; // error
cout << Sum::value << endl; // 12
cout << Sum::value << endl; // 14
return 0;
}
在展開到最后兩個引數時終止,
還可以在展開到0個引數時終止:
#include <iostream>
using namespace std;
template<typename... Args>
struct Sum;
// 基本模板類定義
template<typename First, typename... Rest>
struct Sum<First, Rest...> {
enum {
value = https://www.cnblogs.com/S1mpleBug/archive/2022/10/27/sizeof(First) + Sum::value
};
};
// 遞回終止模板類
template<>
struct Sum<> {
enum {
value = 0
};
};
int main() {
cout << Sum<>::value << endl; // 0
cout << Sum::value << endl; // 4
cout << Sum::value << endl; // 12
cout << Sum::value << endl; // 14
return 0;
}
??注:我一開始對遞回終止條件那里的「展開到2個引數」和「展開到0個引數」的代碼改來改去就是跑不通,發現是「基本模板類定義」那里出了問題,將一開始的
Sum<First>::value改為sizeof(First)即可,對「展開到2個引數」的代碼而言,若不進行「基本模板類定義」這里的修改,那只能保證傳入的引數個數是>=2的偶數個,而當引數個數為奇數個時,就會報錯,這里可以仔細想想為什么,那么對「展開到0個引數」的情況,同理,就不再贅述,
那么說到這里,想必大家都有個疑惑,可以看到不論是模板函式還是模板類的遞回程式,都用到了enum做遞回的數值計算,在模板元編程中,enum是一項重要手段,其主要解決的問題是:
enum的「值由編譯器在編譯期間計算」- 利用遞回演算法和模板特化,可以讓編譯器在計算
enum值時「遞回產生一系列class」
下面直接羅列一個求N的階乘的代碼,可以體會一下如何在模板中借助遞回演算法和模板特化來使用enum實作N的階乘:
#include <iostream>
using namespace std;
template<int N>
class F {
public:
enum {
res = N * F<N-1>::res
};
};
//遞回終止條件
template<>
class F<1> {
public:
enum {
res = 1
};
};
int main() {
cout << F<4>::res << endl; // 24 = 1*2*3*4
return 0;
}
C++模板元編程中有個重要的類叫做std::integral_constant,用來定義型別的常量,其實可以使用std::integral_constant來消除列舉定義,利用std::integral_constant也可獲得「編譯期常量」的特性,原始碼之下了無秘密,std::intergral_constant的原始碼如下:
template<typename _Tp, _Tp __v>
struct integral_constant
{
static constexpr _Tp value = https://www.cnblogs.com/S1mpleBug/archive/2022/10/27/__v;
typedef _Tp value_type;
typedef integral_constant<_Tp, __v> type;
constexpr operator value_type() const noexcept { return value; }
#if __cplusplus > 201103L
#define __cpp_lib_integral_constant_callable 201304
constexpr value_type operator()() const noexcept { return value; } //C++14起
#endif
};
本文對該模板類不做說明,我直接貼出相關文章:[ ?? C++11中type_traits中的基石 - integral_constant ],等后面有機會我再單獨搞一下這里,
因此,可以將前面的Sum例子改為這樣:
#include <iostream>
using namespace std;
//前向宣告
template <typename... Args>
struct Sum;
//基本定義
template <typename First, typename... Rest>
struct Sum<First, Rest...> : std::integral_constant<int, Sum<First>::value + Sum<Rest...>::value>
{};
//遞回終止
template<typename Last>
struct Sum<Last> : std::integral_constant<int, sizeof(Last)>
{};
int main() {
cout << Sum<int, double, short>::value << endl; // 14 = 4+8+2
return 0;
}
繼承方式展開引數包
還可以通過繼承方式來展開引數包,比如下面的例子就是通過繼承的方式去展開引數包:
//整型序列的定義
template<int...>
struct IndexSeq {};
//繼承方式,開始展開引數包
template<int N, int... Indexes>
struct MakeIndexes : MakeIndexes<N-1, N-1, Indexes...> {};
// 模板特化,終止展開引數包的條件
template<int... Indexes>
struct MakeIndexes<0, Indexes...> {
using type = IndexSeq<Indexes...>;
};
int main() {
using T = MakeIndexes<3>::type;
cout << typeid(T).name() << endl; // IndexSeq<0, 1, 2>
return 0;
}
其中MakeIndexes的作用是為了生成一個可變引數模板類的整數序列,「最終輸出」的型別是:struct IndexSeq<0, 1, 2>,
MakeIndexes繼承于自身的一個「特化的」模板類,這個特化的模板類同時也在展開引數包,這個展開程序是通過繼承發起的,直到遇到特化的終止條件展開程序才結束,MakeIndexes<3>::type的展開程序是這樣的:
MakeIndexes<3> : MakeIndexes<2, 2> {}
MakeIndexes<2, 2> : MakeIndexes<1, 1, 2> {}
MakeIndexes<1, 1, 2> : MakeIndexes<0, 0, 1, 2> {
using type = IndexSeq<0, 1, 2>;
}
通過不斷的繼承遞回呼叫,最終得到整型序列IndexSeq<0, 1, 2>,
如果不希望通過繼承方式去生成整型序列,則可以通過下面的方式生成:
#include <iostream>
using namespace std;
//整型序列的定義
template <int...>
struct IndexSeq {};
// 非繼承方式 開始展開引數包
template<int N, int... Indexes>
struct MakeIndexes {
using type = typename MakeIndexes<N-1, N-1, Indexes...>::type;
};
// 模板特化 終止展開引數包的條件
template<int... Indexes>
struct MakeIndexes<0, Indexes...> {
using type = IndexSeq<Indexes...>;
};
int main() {
using T = MakeIndexes<3>::type;
cout << typeid(T).name() << endl;
return 0;
}
我們看到了如何利用「遞回」以及「偏特化」等方法來展開「可變模板引數」,那么實際當中我們會怎么去使用它呢?我們可以用可變模板引數來消除一些重復的代碼以及實作一些高級功能,下面我們來看看可變模板引數的一些應用,
可變引數模板消除重復代碼
C++11 之前如果要寫一個泛化的工廠函式,這個工廠函式能接受任意型別的入參,并且引數個數要能滿足大部分的應用需求的話,我們不得不定義很多重復的模板定義,比如下面的代碼:
template<typename T>
T *Instance() {
return new T();
}
template<typename T, typename T0>
T *Instance(T0 arg0) {
return new T(arg0);
}
template<typename T, typename T0, typename T1>
T *Instance(T0 arg0, T1 arg1) {
return new T(arg0, arg1);
}
template<typename T, typename T0, typename T1, typename T2>
T *Instance(T0 arg0, T1 arg1, T2 arg2) {
return new T(arg0, arg1, arg2);
}
template<typename T, typename T0, typename T1, typename T2, typename T3>
T *Instance(T0 arg0, T1 arg1, T2 arg2, T3 arg3) {
return new T(arg0, arg1, arg2, arg3);
}
template<typename T, typename T0, typename T1, typename T2, typename T3, typename T4>
T *Instance(T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) {
return new T(arg0, arg1, arg2, arg3, arg4);
}
struct A {
A(int) {}
};
struct B {
B(int, double) {}
};
A *pa = Instance<A>(1);
B *pb = Instance<B>(1, 2);
可以看到這個泛型工廠函式存在大量的重復的模板定義,并且限定了模板引數,用可變模板引數可以消除重復,同時去掉引數個數的限制,代碼很簡潔,通過可變引數模板優化后的工廠函式如下:
template<typename T, typename... Args>
T *Instance(Args... args) {
return new T(args...);
}
在上面的實作代碼T *Instance(Args... args)中,Args是值拷貝的,存在性能損耗,可以通過完美轉發來消除損耗,代碼如下:
template<typename T, typename... Args>
T *Instance(Args&&... args) {
return new T(std::forward<Args>(args)...);
}
struct A {
A(int) {}
};
struct B {
B(int, double) {}
};
A *pa = Instance<A>(1);
B *pb = Instance<B>(1, 2);
可變引數模板實作泛化的delegate
C++ 中沒有類似C#的委托,我們可以借助可變模板引數來實作一個,C#中的委托的基本用法是這樣的:
delegate int AggregateDelegate(int x, int y); // 宣告委托型別
int Add(int x, int y) { return x + y; }
int Sub(int x, int y) { return x - y; }
AggregateDelegate add = Add;
add(1, 2); // 呼叫委托物件求和
AggregateDelegate sub = Sub;
sub(2, 1); // 呼叫委托物件相減
C#中的委托的使用需要先定義一個委托型別,這個委托型別不能泛化,即委托型別一旦宣告之后就不能再用來接受其它型別的函式了,比如這樣用:
int Fun(int x, int y, int z) { return x + y + z; }
int Fun1(string s, string r) { return s.Length + r.Length; }
AggregateDelegate fun = Fun; //編譯報錯,只能賦值相同型別的函式
AggregateDelegate fun1 = Fun1; //編譯報錯,引數型別不匹配
這里不能泛化的原因是宣告委托型別的時候就限定了「引數型別」和「個數」,在C++11里不存在這個問題了,因為有了可變模板引數,它就代表了任意型別和個數的引數了,下面讓我們來看一下如何實作一個功能更加泛化的C++版本的委托(這里為了簡單起見只處理成員函式的情況,并且忽略const、volatile和const volatile成員函式的處理),
#include <iostream>
using namespace std;
template<typename T, typename R, typename... Args>
class MyDelegate {
public:
MyDelegate(T *t, R (T::*f)(Args...)) : m_t(t), m_f(f) {}
R operator()(Args&&... args) {
return (m_t->*m_f)(std::forward<Args>(args)...);
}
private:
T *m_t;
R (T::*m_f)(Args...); // 函式指標
};
template<typename T, typename R, typename... Args>
MyDelegate<T, R, Args...> CreateDelegate(T *t, R (T::*f)(Args...)) {
return MyDelegate<T, R, Args...>(t, f);
}
struct A {
void Fun(int i ) { cout << i << endl; }
void Fun1(int i, double j) { cout << i+j << endl; }
};
int main() {
A a;
auto d = CreateDelegate(&a, &A::Fun); // 創建委托
d(1); // 呼叫委托 將輸出1
auto d1 = CreateDelegate(&a, &A::Fun1); // 創建委托
d1(1, 2.5); // 呼叫委托 將輸出3.5
return 0;
}
MyDelegate實作的「關鍵」是內部定義了一個能接受任意型別和引數個數的「萬能函式」:R (T::*m_f)(Args...),正是由于「可變模板引數」的特性,所以我們才能夠讓這個m_f接受任意引數,
總結
使用可變模板引數的這些技巧相信讀者看了會有耳目一新之感,使用可變模板引數的關鍵是如何展開引數包,展開引數包的程序是很精妙的,體現了泛化之美、遞回之美,正是因為它具有神奇的「魔力」,所以我們可以更泛化的去處理問題,比如用它來消除重復的模板定義,用它來定義一個能接受任意引數的「萬能函式」等,其實,可變模板引數的作用遠不止文中列舉的那些作用,它還可以和其它C++11特性結合起來,比如type_traits、std::tuple等特性,發揮更加強大的威力,
我想后面我得系統學習一下C++模板元編程了,這破爛玩意兒太裝逼了,我好喜歡!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/521820.html
標籤:其他
上一篇:JAVA常見基礎知識點
下一篇:設計模式---模板方法模式
