C++11 可变参数模版详解

· · 科技·工程

可变参数模板

函数模板

给没有接触过函数模板的同学讲一下函数模板。

首先考虑下面一段代码。

#include<bits/stdc++.h>
using namespace std;

//Some function definitions...

int main(){
    out(1);
    out(1.4);
    out(970132ll);
    out('H');
    out("Hero_Broom");
    return 0;
}

我们计划使用 out 函数来输出给入的变量。在上面的代码中,如果想要输出上面的信息,我们需要写五个 out 函数,来对应每一种类型。这是非常不方便的,如果我们还想要支持更多类型,我们岂不是要写一大堆 out 函数?

这就引入了模板函数了——它可以接受任何类型,具体实现如下:

#include<bits/stdc++.h>
using namespace std;

template <class T>
void out(T v){
    cout<<v<<endl;
}

int main(){
    out(1);
    out(1.4);
    out(970132LL);
    out('H');
    out("Hero_Broom");
    return 0;
}

其中的 class 可替换为 typename

我们只需要记住这条语句即可,接下来函数中的 T 都可以看做是一个类型,传入的值为 1.4T 就为 float,传入的值为 970132LLT 就为 long\ long

在一个函数中,我们可以同时定义多种类型,例如下面的程序:

#include<bits/stdc++.h>
using namespace std;

template <class T,class S>
double devide(T a,S b){
    return a/b;
}

int main(){
    cout<<devide(1,4)<<endl;
    cout<<devide(1LL,4.0)<<endl;
    return 0;
}

输出:

0
0.25

--------------------------------
Process exited after 0.03401 seconds with return value 0
请按任意键继续. . .

学习完了函数模板,就可以看可变参数模版了。

引入

在 C++11 之前,类模板和函数模板只支持固定的参数。也就是说,你不能在同一个函数里改变参数的数目,就比如下面的代码:

#include<bits/stdc++.h>
using namespace std;

//The definition of function 'show_list'.

int main(){
    show_list(1);
    show_list("Hero_Broom",970132);
    show_list(1234567,'\n',"It is just a test!!!");
    return 0;
}

如果我们想要让 show_list 函数输出传入的所有参数,并在每个参数的输出之间加上空格,那么我们就必须定义三个甚至更多的函数,来分别对应传入参数个数有一个,两个和三个的情况。

template <typename A>
void show_list(A a){
    cout<<a<<" ";
}

template <typename A,typename B>
void show_list(A a,B b){
    cout<<a<<" "<<b<<" ";
}

template <typename A,typename B,typename C>
void show_list(A a,B b,C c){
    cout<<a<<" "<<b<<" "<<c<<" ";
}

如果只是看上面的函数的话,可能你觉得不就是多写几个函数吗,那如果传入的参数有十个,函数写起来就会非常麻烦。

在 C++11 以后,模板功能被增强,允许模板定义中包含零到任意个模板参数,这也就是可变参数模板了。可变参数模板的加入使得 C++11 的功能变得更加强大,而由此也带来了许多神奇的用法。

可变参数模板的用法

可变参数模板与普通模板的写法比较类似,只需要在 typename 后面加上省略号 ... 即可。我们称代码中 args 为一个形参参数包

template <typename T,typename... types>
void show_list(T val,types... args){

}

需要注意的是,这里的 types... args 传入的参数个数是大于等于零个的,而在前面还有一个 val 参数。加上这个 val,正割函数可接受的参数个数范围为 \left [1 , + \infty \right )

也就是说如果我们使用下面的语句:

show_list();

程序就会报错。所以需要再加上一个额外的定义,专门处理没有传入参数的情况。

void show_list(){
    cout<<endl;
}

如果我们要获取参数包的大小,则可以通过 sizeof 实现,具体代码如下:

template <typename T,typename... types>
void show_list(T val,types... args){
    printf("The size of the data pack is: %d\n",sizeof...(args));
}

那么有人可能就想到了:既然我们已经知道参数包的大小了,我们能不能向数组一样遍历它来输出呢?由此我们写出了下面的代码:

template <typename... types>
void show_list(types... args){
    for(int i=0;i<sizeof...(args);i++){
        cout<<args[i]<<" ";
    }
}

但遗憾的是,这段代码会出现报错信息,我们不能像遍历数组一样去遍历参数包。

[Error] parameter packs not expanded with '...':

所以我们就需要用到参数包的递归展开来实现输出的功能了。

参数包的展开

首先注意到上面的函数中除了参数包 args 外,我们还在前面单独提出一个参数 T val,那你可能就会问了:为什么我们不能直接把数据全部丢进参数包里面,还要单独拎出一个 val 出来呢。先来看看我们是怎样实现参数包的输出的:

  1. 输出参数列表中第一个参数;

  2. 递归输出后面的参数,具体就要用到我们刚才说到的参数包了。

那看到这里你可能就明白了,我们要先输出第一个参数的话,就不能只使用参数包了,因为它是不能直接像数组一样访问的。

具体的代码实现如下所示,可以看到函数中先输出了第一个参数,然后再 show_list 输出剩下参数包中的参数。

#include<bits/stdc++.h>
using namespace std;

void show_list(){
    cout<<endl;
}

template <class T,class... types>
void show_list(const T val,const types... args){
    cout<<val<<" ";
    show_list(args...);
}

int main(){
    show_list(1);
    show_list("Hero_Broom",970132);
    show_list(1234567,'\n',"It is just a test!!!");
    return 0;
}

输出:

1
Hero_Broom 970132
1234567
 It is just a test!!!

--------------------------------
Process exited after 0.03312 seconds with return value 0
请按任意键继续. . .

~~ 那么可能就有细心的同学发现了,~~ 为什么每一条输出之间都有一个换行呢,在 show_list 之间也没有换行啊?这时候,我们可以拆开来一步一步看。

假设让我们调用 show_list(1234567,'a',"It is just a test!!!")

  1. show_list(1234567,'a',"It is just a test!!!") 输出 1234567,递归 show_list('a',"It is just a test!!!")

  2. show_list('a',"It is just a test!!!") 输出 1234567 s,递归 show_list("It is just a test!!!")

  3. show_list("It is just a test!!!") 输出 1234567 a It is just a test!!!,递归 show_list()

  4. show_list(),什么都没有,那输出什么呢,前面我们定义了 void show_list(){cout<<endl;},所以这里就会输出一个换行,并结束递归。

所以这就是为什么会有这个换行符的原因,也同时解决了如何结束递归的问题。

也就是说,如果在上面定义无参的 show_list 时去掉 cout<<endl; 这句,换行也就不会出现了。

带参递归终止函数

上面的递归终止函数,也就是那一个无参的 show_list 函数。它是没有参数的,所以称它为无参递归终止函数带参递归终止函数带有一个参数,用途和无参递归终止函数一样,都用来让递归终止;原理也和上面无参递归终止函数类似,所以我们可以直接看代码:

#include<bits/stdc++.h>
using namespace std;

void show_list(){
    cout<<endl;
}

template <class T>
void show_list(T val){
    cout<<val<<" ";
}

template <class T,class... types>
void show_list(const T val,const types... args){
    cout<<val<<" ";
    show_list(args...);
}

int main(){
    show_list(1);
    show_list("Hero_Broom",970132);
    show_list(1234567,'\n',"It is just a test!!!");
    return 0;
}

输出:

1 Hero_Broom 970132 1234567
 It is just a test!!!
--------------------------------
Process exited after 0.03364 seconds with return value 0
请按任意键继续. . .

可以看到,这里只有一个换行,而这个换行是上面代码中 show_list(1234567,'\n',"It is just a test!!!") 中输出的,那么上面的代码中为什么加了一个带参递归终止函数就没有输出换行了呢,我们也可以一步一步地模拟。

还是以 show_list(1234567,'a',"It is just a test!!!") 为例:

  1. show_list(1234567,'a',"It is just a test!!!") 输出 1234567,递归 show_list('a',"It is just a test!!!")

  2. show_list('a',"It is just a test!!!") 输出 1234567 s,递归 show_list("It is just a test!!!")

  3. show_list("It is just a test!!!"),这个时候条用的就是上面的带参归终止函数了。所以输出完 1234567 a It is just a test!!! 之后就不会再递归了。

在这个过程中,可以看到相较于上面少了第四步,因此就不会再使用无参递归终止函数,也就不会输出换行了。

所以需要注意的是,编写带参递归终止函数需要使用函数模板,因为我们并不知道最后一个参数是什么类型的。

但这种方法有一个弊端就是,我们在调用 show_list 函数时必须传入至少一个参数,否则就会报错。因为此时无论是调用递归终止函数还是展开函数,都需要至少传入一个参数。

通过逗号表达式展开参数包

这个部分比较难理解,我尽量讲清楚一些。我自己学的时候都有点懵。

在学习用逗号表达式展开参数包前,我们当然需要先了解逗号运算符的用法和性质。

这个时候,我们来看一下代码。

#include<bits/stdc++.h>
using namespace std;

template<class T>
void out(const T& t){
    cout<<t<<" ";
}

template<class ...types>
void show_list(types... args){
    int arr[] = { (out(args), 0)... };
    cout<<endl;
}

int main(){
    show_list(1234567,'a',"Hero_Broom");
    return 0;
}

我们一步一步来看:

  1. 首先,将逗号表达式的最后一个表达式设为 0,这样整个逗号表达式的返回值就是 0,正好对应 arr 数组的类型 int

  2. 然后将逗号表达式的第一个表达式设为输出函数,用来输出参数包里的每一个参数

  3. 在初始化数组时,会从左到右计算初始化列表里的每一个表达式的值(在这里都为零)。我们并不关心最后 arr 数组里的数,关键在于初始化数组时对于参数包里每一个参数的输出

上面没看懂的一定要多看几遍。

在这里,{ (out(args), 0)... } 相当于对参数包里每一个参数都执行一遍 out 函数,相当于:

\{ \ (\ out(arg_1),0\ )\ ,\ (\ out(arg_2),0\ )\ ,\ ...\ ,(\ out(arg_n),0\ )\ ,\ \}

程序在初始化 arr 数组时,会先计算 (out(arg1),0) 的值。这时候就顺便运行了一遍 out 函数,把第一个参数的值输出了,而 arr[0] 的值就变成了 0

这样,在初始化整个 arr 数组时,程序就把传入的参数包中的参数顺便输出了。

当然,如果你觉得使用 (out(args),0) 太麻烦的话,也可以写一个返回 intout 函数,因为本质上写这些的目的就是让初始化值为 int 类型。

#include<bits/stdc++.h>
using namespace std;

template<class T>
int out(const T& t){
    cout<<t<<" ";
    return 0;
}

template<class ...types>
void show_list(types... args){
    int arr[] = {out(args)...};
    cout<<endl;
}

int main(){
    show_list(1234567,'a',"Hero_Broom");
    return 0;
}

输出:

1234567 a Hero_Broom

--------------------------------
Process exited after 0.1315 seconds with return value 0
请按任意键继续. . .

与上面使用逗号表达式效果是一样的。

可变参数模版的应用

当我们开发程 (you) 序 (xi) 时,经常会需要日志。日志的形式多种多样,有时候只需要记录一个信息,而有时候要记录很多信息,所以这个时候使用可变参数模板实在适合不过的了。

#include<bits/stdc++.h>
#include<windows.h>
using namespace std;

ofstream off;

void show_list(){
    off<<endl;
}

template <class T,class... types>
void show_list(const T val,const types... args){
    off<<val<<" ";
    show_list(args...);
}

template <class... types>
void Write_Log(const char* file_name,types... args){
    off.open(file_name);
    time_t ti=time(0);
    tm* _t=localtime(&ti);
    off<<"["<<_t->tm_year+1900<<"->"<<_t->tm_mon<<"->"<<_t->tm_mday<<"\t";
    off<<_t->tm_hour<<":"<<_t->tm_min<<":"<<_t->tm_sec<<"]:";
    show_list(args...);
}

int main(){
    Write_Log("log.log","The programme begins.");
    Sleep(1000);
    Write_Log("log.log","The programme ends after ",clock()," ms.");
    return 0;
}

文件 log.log 中的内容:

[2024/10/19     19:50:29]:The programme begins.
[2024/10/19     19:50:30]:The programme ends after  1014  ms.

总结

这篇文章讲的只是关于可变参数模版最基础的只是,要熟练地运用,我们就需要多写代 (you) 码 (xi),这样才能加强对知识的了解。

参考资料:

  1. CSDN 【C++】C++11 可变参数模板(函数模板、类模板)作者:Yngz_Miao

  2. CSDN 【C++11】可变参数模版 /lambda 表达式 / 包装器 作者:KL4180

  3. CSDN 【C++11】可变参数模版 作者:jjrenhai