C++11引入了一个很强大的模板功能,即可变模板参数(variadic templates),
它允许定义出可变参数个数,同时可变参数类型的模板,看一个简单的例子
1 #include <iostream>
2 #include <cstdio>
3 using namespace std;
4
5 template <typename Args> // 通过typename声明的Args称为模板参数包,注意是写在typename后面的
6 void func(const Args& args) // 这里的const Args&称为函数参数类型包,而声明出的args称为函数参数包
7 {
8 printf(args); // 这里的args称为对函数参数包的展开
9 }
10
11 int main(void)
12 {
13 func("%s %d", "Hello world", 123);
14 }
15
输出为
上面的展示很简单明确,现在我们基本知道可变模板参数是个什么东西了。那么它有什么用呢,下面我们来做一些稍微复杂一点的事,如果我们不用printf这样的接受可变长参数列表的函数作为输出,而使用cout该怎么修改上面的代码呢?下面我们稍作修改,通过递归调用把参数一个一个拆出来。
1 #include <iostream>
2 #include <cstdio>
3 using namespace std;
4
5 // 递归终止
6 void func()
7 {
8
9 }
10
11 template <typename T, typename Args>
12 void func(const T& obj, const Args& args)
13 {
14 cout << obj << " ";
15 func(args); // 递归调用
16 }
17
18 int main(void)
19 {
20 func("Hello world", 123); //因为改成通过cin,前面的格式化字符串删掉了
21 }
通过增加在模板参数包前面增加一个参数T接受一个参数,再把后面的所有参数递归调用,就实现了一个一个拆出来,最后的调用中args...是可以为0个参数的,这时候调用到了上面的重载函数func()什么都不做。我们可能会想,能不能不写一个空的func()直接在模板函数里面判断args携带的参数个数来终止递归呢,答案是:可以的,但没那么简单。取得可变模板参数的个数可以使用sizeof...()或tuple_size<>::value,下面实现一下
1 #include <iostream>
2 #include <cstdio>
3 using namespace std;
4
5 template <typename T, typename Args>
6 void func(const T& obj, const Args& args)
7 {
8 cout << obj << " ";
9 if (sizeof(Args) > 0) {
10 func(args); // 递归调用
11 }
12 }
13
14 int main(void)
15 {
16 func("Hello world", 123); //因为改成通过cin,前面的格式化字符串删掉了
17 }
18
上面的代码试图通过判断sizeof...(Args)是否大于0决定是否继续递归,但是上面的代码会编译错误,因为if的判断是在运行期才做的,对func(args...)的调用在编译时就要有匹配的函数存在,而当args一个参数都没有时,编译器找不到匹配的函数进行调用(func至少接受一个参数T)。
所以问题的关键点是if分支是运行期的分支,如果我们做一个编译期的分支不就可以解决了么。
1 #include <iostream>
2 #include <cstdio>
3 using namespace std;
4
5 template <typename T, typename Args>
6 void func(const T& obj, const Args& args);
7
8 // 声明一个带有一个bool参数的模板struct
9 template<bool Flag>
10 struct If_Func;
11
12 // 下面对bool参数分别进行true/false两种特化实现,以此实现编译期的分支
13 template<>
14 struct If_Func<true>
15 {
16 template<typename Args>
17 static void call (const Args& args) {
18 func(args);
19 }
20 };
21
22 template<>
23 struct If_Func<false>
24 {
25 template<typename Args>
26 static void call(const Args& args) {
27
28 }
29 };
30
31 template <typename T, typename Args>
32 void func(const T& obj, const Args& args)
33 {
34 cout << obj << " ";
35 constexpr bool flag = sizeof(Args) > 0;
36 If_Func<flag>::call(args);
37 }
38
39 int main(void)
40 {
41 func("Hello world", 123); //因为改成通过cin,前面的格式化字符串删掉了
42 }
上面引入了一个If_Func,这个东西从语法层面来说就是类模板和模板特化,不是新东西,但是这种用法一般被称为模板元编程,C++1x里有一个头文件<type_traits>里面提供了很多模板可以帮助我们进行各种判断,比如判断一个类型是否指针,是否POD,一个类是否是另一个类的子类,等等,可以辅助我们进行模板元编程,有兴趣的自行搜索了解。
下面我们讲另一个东西,元组(tuple),它可以看成是对std::pair的n个元素扩充
#include <iostream>
#include <tuple>
using namespace std;
int main(void)
{
tuple<int, double, string> t = make_tuple(1, 3.14, "Hello world");
cout << typeid(t).name() << endl;
cout << std::tuple_size<decltype(t)>::value << endl; // 输出tuple的元素个数
cout << std::get<0>(t) << endl; // 输出tuple的第0个元素
cout << std::get<1>(t) << endl; // 输出tuple的第1个元素
cout << std::get<2>(t) << endl; // 输出tuple的第2个元素
}
这里不展开讲tuple的用法,我们注意,tuple的模板参数其实类型和数量都是可变的,事实上它就是利用了可变模板参数来实现的。怎么实现呢,大概是这样
template<typename Args>
class my_tuple;
template<>
class my_tuple<>
{
};
template<typename T, typename Args>
class my_tuple<T, Args> : public my_tuple<Args>
{
public:
protected:
T _obj;
};
通过递归继承的方式,每次把一个参数拆出来作为成员变量,其余的参数丢给基类,直到参数为空。
标准库并没有实现tuple的输出,现在产生一个问题,我们自己能否实现一个tuple的输出函数呢。
要实现的就是如果我们使用
cout << make_tuple(123, 3.14, "Hello") << endl;
可以像python里面
print( (123, 3.14, "Hello") )
输出元组一样,打印出类似 (123, 3.14, 'Hello')
我们要实现的函数原型是这样
template <typename... Args>
ostream& operator << (ostream& out, const tuple<Args...> t);
直接给出代码,里面再次用到了所谓的模板元编程技巧。
#include <iostream>
#include <string>
#include <tuple>
#include <sstream>
using namespace std;
template <size_t N>
struct PrintHelper;
template <>
struct PrintHelper<1>
{
template<typename Args>
static void recursive_print(ostream& out, const tuple<Args> t)
{
out << "(" << std::get<0>(t) << ", ";
}
};
template <size_t N>
struct PrintHelper
{
template<typename Args>
static void recursive_print(ostream& out, const tuple<Args> t)
{
PrintHelper<N - 1>::recursive_print(out, t);
out << std::get<N - 1>(t) << ", ";
}
template<typename Args>
static void print(ostream& out, const tuple<Args> t)
{
PrintHelper<N - 1>::recursive_print(out, t);
out << std::get<N - 1>(t) << ")";
}
};
template <typename Args>
ostream& operator << (ostream& out, const tuple<Args> t)
{
PrintHelper<tuple_size<decltype(t)>::value >::print(out, t);
// PrintHelper<sizeof(Args)>::print(out, t); // 和上面等价
return out;
}
int main(void)
{
cout << std::make_tuple(123, 3.14, "Hello") << endl;
}
事实上这里会输出
Hello的两端并没有引号,如果想加上引号,可以再次利用模板元编程技巧通过编译时的分支把string类型的输出方法特化,此处不再给出具体代码,留给读者自行探索。