posts - 225, comments - 62, trackbacks - 0, articles - 0
   :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

可变模版参数(variadic templates)

Posted on 2019-11-09 13:23 魔のkyo 阅读(431) 评论(0)  编辑 收藏 引用 所属分类: STLC++
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
输出为
Hello world 123
上面的展示很简单明确,现在我们基本知道可变模板参数是个什么东西了。那么它有什么用呢,下面我们来做一些稍微复杂一点的事,如果我们不用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
<intdoublestring> t = make_tuple(13.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& outconst tuple<Args> t)
    {
        
out << "(" << std::get<0>(t) << "";
    }
};

template 
<size_t N>
struct PrintHelper
{
    template
<typename Args>
    
static void recursive_print(ostream& outconst tuple<Args> t)
    {
        PrintHelper
<- 1>::recursive_print(out, t);
        
out << std::get<- 1>(t) << "";
    }

    template
<typename Args>
    
static void print(ostream& outconst tuple<Args> t)
    {
        PrintHelper
<- 1>::recursive_print(out, t);
        
out << std::get<- 1>(t) << ")";
    }
};


template 
<typename Args>
ostream
& operator << (ostream& outconst 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(1233.14"Hello"<< endl;
}
事实上这里会输出
(123, 3.14, Hello)
Hello的两端并没有引号,如果想加上引号,可以再次利用模板元编程技巧通过编译时的分支把string类型的输出方法特化,此处不再给出具体代码,留给读者自行探索。
只有注册用户登录后才能发表评论。