可能學了假的編程?C++新標準難點解析之可變模板參數
前言
C++的新特性--可變模版參數(variadic templates)是C++新增的最強大的特性之一,它對參數進行了高度泛化,它能表示0到任意個數、任意類型的參數。相比C++98/03,類模版和函數模版中只能含固定數量的模版參數,可變模版參數無疑是一個巨大的改進。然而由于可變模版參數比較抽象,使用起來需要一定的技巧,所以它也是C++中最難理解和掌握的特性之一。雖然掌握可變模版參數有一定難度,但是它卻是C++11中最有意思的一個特性,本文希望帶領讀者由淺入深地認識和掌握這一特性,同時也會通過一些實例來展示可變參數模版的一些用法。
變模版參數的展開
可變參數模板和普通模板的語義是一樣的,只是寫法上稍有區別,聲明可變參數模板時需要在typename或class后面帶上省略號“...”。比如我們常常這樣聲明一個可變模版參數:template
- template <class... T>
- void f(T... args);
省略號的作用:
1.聲明一個參數包T... args,這個參數包中可以包含0到任意個模板參數; 2.在模板定義的右邊,可以將參數包展開成一個一個獨立的參數。
省略號的參數稱為“參數包”,它里面包含了0到N(N>=0)個模版參數。我們無法直接獲取參數包args中的每個參數的,只能通過展開參數包的方式來獲取參數包中的每個參數,這是使用可變模版參數的一個主要特點,也是最大的難點,即如何展開可變模版參數。
可變模板參數分類:
1.可變模版參數函數
2.可變模版參數類
打印可變模版參數函數的參數個數
- #include <iostream>
- #include <string>
- using namespace std;
- template <class ...Type>
- void print(Type ...data)
- {
- cout << sizeof...(data) << endl;
- }
- int main()
- {
- print();
- print(1);
- print(1, "ILoveyou");
- print(1, 2, 3.4, "IMissyou");
- return 0;
- }
上面的例子中,print()沒有傳入參數,所以參數包為空,輸出的size為0,后面兩次調用分別傳入兩個和三個參數,故輸出的size分別為2和3。由于可變模版參數的類型和個數是不固定的,所以我們可以傳任意類型和個數的參數給函數print。這個例子只是簡單的將可變模版參數的個數打印出來,如果我們需要將參數包中的每個參數打印出來的話就需要通過一些方法了。
展開可變模版參數函數的方法一般有兩種:
1.通過遞歸函數來展開參數包。
2.逗號表達式來展開參數包。
遞歸方式展開參數包
通過遞歸函數展開參數包,需要提供一個參數包展開的函數和一個遞歸終止函數,遞歸終止函數正是用來終止遞歸的,如下面的例子:
- #include <iostream>
- using namespace std;
- //遞歸終止函數
- void print()
- {
- cout << "遞歸終止函數" << endl;
- }
- //展開函數
- template <class T,class ...Type>
- void print(T data,Type...exData)
- {
- cout << data << endl;
- print(exData...);
- }
- int main()
- {
- print(1, 2, 3, 4);
- return 0;
- }
上例會輸出每一個參數,直到為空時輸出"遞歸終止函數"。展開參數包的函數有兩個,一個是遞歸函數,另外一個是遞歸終止函數,參數包exData...在展開的過程中遞歸調用自己,每調用一次參數包中的參數就會少一個,直到所有的參數都展開為止,當沒有參數時,則調用非模板函數print終止遞歸過程。當然上述終止函數也可以寫成帶參數函數模板:
- template <class T>
- void print(T data)
- {
- cout<<data<endll
- }
接下來用模板函數作為終止函數寫一個不限參求和函數,具體實現代碼如下:
- #include <iostream>
- using namespace std;
- //遞歸終止函數
- template <typename Type>
- Type sum(Type t)
- {
- return t;
- }
- //展開函數
- template <class T,class ...Type>
- T sum(T a, Type ...b)
- {
- return a + sum<T>(b...);
- }
- int main()
- {
- cout << sum(1, 2, 3, 4) << endl;
- cout << sum(1, 2, 3) << endl;
- return 0;
- }
sum在展開參數包的過程中將各個參數相加求和,參數的展開方式和前面的打印參數包的方式是一樣的。
逗號表達式展開參數包
遞歸函數展開參數包是一種標準做法,也比較好理解,但也有一個缺點,就是必須要一個重載的遞歸終止函數,即必須要有一個同名的終止函數來終止遞歸,這樣可能會感覺稍有不便。有沒有一種更簡單的方式呢?其實還有一種方法可以不通過遞歸方式來展開參數包,這種方式需要借助逗號表達式和初始化列表。比如前面打印函數可以改成這樣:
- #include <iostream>
- using namespace std;
- //遞歸終止函數
- template <class T>
- void print(T data)
- {
- cout << data << "\t";
- }
- template <class ...Type>
- void print(Type ...exData)
- {
- int array[] = { (print(exData),0)... };
- }
- int main()
- {
- print(1, 2, 3);
- cout << endl;
- print("張三", 1, 3);
- return 0;
- }
這個數組的目的純粹是為了在數組構造的過程展開參數包。我們可以把上面的例子再進一步改進一下,將函數作為參數,就可以支持lambda表達式了,從而可以少寫一個遞歸終止函數了,具體代碼如下:
C++中新特性之:initializer_list詳解
C++11提供的新類型,定義在
- template< class T >
- class initializer_list;
下面稍微介紹一下initializer_list
一個initializer_list當出現在以下兩種情況的被自動構造:
- 當初始化的時候使用的是大括號初始化,被自動構造。包括函數調用時和賦值
- 當涉及到for(initializer: list),list被自動構造成initializer_list對象
也就是說initializer_list對象只能用大括號{}初始化。拷貝一個initializer_list對象并不會拷貝里面的元素。其實只是引用而已。而且里面的元素全部都是const的。下面一個例子可以幫助我們更好地理解如何使用initializer_list:
- #include <iostream>
- #include <vector>
- #include <initializer_list>
- using namespace std;
- template <class T>
- struct S
- {
- vector<T> v;
- S(initializer_list<T> l) : v(l) {
- cout << "constructed with a " << l.size() << "-elements lists" << endl;
- }
- void append(std::initializer_list<T> l) {
- v.insert(v.end(), l.begin(), l.end());
- }
- pair<const T*, size_t> c_arr() const {
- return { &v[0], v.size() };
- }
- };
- template <typename T>
- void templated_fn(T arg) {
- for (auto a : arg)
- cout << a << " ";
- cout << endl;
- }
- int main()
- {
- S<int> s = { 1, 2, 3, 4, 5 };
- s.append({ 6, 7 , 8 });
- for (auto n : s.v)
- cout << ' ' << n;
- cout << endl;
- for (auto x : { -1, -2, 03 })
- cout << x << " ";
- cout << endl;
- auto al = { 10, 11, 12 };
- templated_fn<initializer_list<int> >({ 7, 8, 9 });
- templated_fn<vector<int>>({ 3, 5, 7 });
- return 0;
- }
可變模版參數類
std::tuple就是一個可變模板類
- template< class... Types >
- class tuple;
這個可變參數模板類可以攜帶任意類型任意個數的模板參數:
- tuple<int> tp1 = std::make_tuple(1);
- tuple<int, double> tp2 = std::make_tuple(1, 2.5);
- tuple<int, double, string> tp3 = std::make_tuple(1, 2.5, “”);
可變參數模板的模板參數個數可以為0個,所以下面的定義也是也是合法的:
- tuple<> tp;
可變參數模板類的參數包展開的方式和可變參數模板函數的展開方式不同,可變參數模板類的參數包展開需要通過模板特化和繼承方式去展開,展開方式比可變參數模板函數要復雜。
模版偏特化和遞歸方式來展開參數包
基本的可變參數模板類
- //前向聲明
- template<typename... Args>
- struct Sum;
- //基本定義
- template<typename First, typename... Rest>
- struct Sum<First, Rest...>
- {
- enum { value = Sum<First>::value + Sum<Rest...>::value };
- };
- //遞歸終止
- template<typename Last>
- struct Sum<Last>
- {
- enum { value = sizeof (Last) };
- };
- int main()
- {
- cout << Sum<int, double, short>::value << endl;
- 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...>
- {
- typedef IndexSeq<Indexes...> type;
- };
- int main()
- {
- using T = MakeIndexes<3>::type;
- cout <<typeid(T).name() << endl;
- return 0;
- }
可變參數模版消除重復代碼
C++11之前如果要寫一個泛化的工廠函數,這個工廠函數能接受任意類型的入參,并且參數個數要能滿足大部分的應用需求的話,我們不得不定義很多重復的模版定義,比如下面的代碼:
- #include <iostream>
- using namespace std;
- template<typename T, typename... Args>
- T* Instance(Args... args)
- {
- return new T(args...);
- }
- class A
- {
- public:
- A(int a) :a(a) {}
- A(int a, int b) :a(a) {}
- A(int a, int b,string c) :a(a) {}
- void print()
- {
- cout << a << endl;
- }
- int a;
- };
- class B
- {
- public:
- B(int a, int b) :a(a), b(b) {}
- void print()
- {
- cout << a << endl;
- cout << b << endl;
- }
- int a;
- int b;
- };
- int main()
- {
- A* pa = Instance<A>(1);
- B* pb = Instance<B>(1, 2);
- pa->print();
- pb->print();
- pa = Instance<A>(100, 2);
- pa->print();
- pa = Instance<A>(100, 2,"Loveyo");
- pa->print();
- return 0;
- }