C++11可变参数模板

所谓可变参数,指的是参数的个数和类型都可以是任意的。提到参数,大家会第一时间想到函数参数,除此之外 C++ 的模板(包括函数模板和类模板)也会用到参数。

对于函数参数而言,C++ 一直都支持为函数设置可变参数,最典型的代表就是 printf() 函数,它的语法格式为:

int printf ( const char * format, ... );

...就表示的是可变参数,即 printf() 函数可以接收任意个参数,且各个参数的类型可以不同,例如:

printf("%d", 10);
printf("%d %c",10, 'A');
printf("%d %c %f",10, 'A', 1.23);

我们通常将容纳多个参数的可变参数称为参数包。借助 format 字符串,printf() 函数可以轻松判断出参数包中的参数个数和类型。

下面的程序中,自定义了一个简单的可变参数函数:

#include <iostream>
#include <cstdarg>
//可变参数的函数
void vair_fun(int count, ...)
{
    va_list args;
    va_start(args, count);
    for (int i = 0; i < count; ++i)
    {
        int arg = va_arg(args, int);
        std::cout << arg << " ";
    }
    va_end(args);
}

int main()
{
    //可变参数有 4 个,分别为 10、20、30、40
    vair_fun(4, 10, 20, 30,40);
    return 0;
}

程序中的 vair_fun() 函数有 2 个参数,一个是 count,另一个就是 … 可变参数。我们可以很容易在函数内部使用 count 参数,但要想使用参数包中的参数,需要借助<cstdarg>头文件中的 va_start、va_arg 以及 va_end 这 3 个带参数的宏:

  • va_start(args, count):args 是 va_list 类型的变量,我们可以简单的将其视为 char * 类型。借助 count 参数,找到可变参数的起始位置并赋值给 args;
  • va_arg(args, int):调用 va_start 找到可变参数起始位置的前提下,通过指明参数类型为 int,va_arg 就可以将可变参数中的第一个参数返回;
  • va_end(args):不再使用 args 变量后,应及时调用 va_end 宏清理 args 变量。

注意,借助 va_arg 获取参数包中的参数时,va_arg 不具备自行终止的能力,所以程序中借助 count 参数控制 va_arg 的执行次数,继而将所有的参数读取出来。控制 va_arg 执行次数还有其他方法,比如读取到指定数据时终止。

使用 … 可变参数的过程中,需注意以下几点:

  1. … 可变参数必须作为函数的最后一个参数,且一个函数最多只能拥有 1 个可变参数。
  2. 可变参数的前面至少要有 1 个有名参数(例如上面例子中的 count 参数);
  3. 当可变参数中包含 char 类型的参数时,va_arg 宏要以 int 类型的方式读取;当可变参数中包含 short 类型的参数时,va_arg 宏要以 double 类型的方式读取。

需要注意的是,… 可变参数的方法仅适用于函数参数,并不适用于模板参数。C++11 标准中,提供了一种实现可变模板参数的方法。

可变参数模板

C++ 11 标准发布之前,函数模板和类模板只能设定固定数量的模板参数。C++11 标准对模板的功能进行了扩展,允许模板中包含任意数量的模板参数,这样的模板又称可变参数模板

1) 可变参数函数模板

先讲解函数模板,如下定义了一个可变参数的函数模板:

template<typename... T>
void vair_fun(T...args) {
    //函数体
}

模板参数中, typename(或者 class)后跟 … 就表明 T 是一个可变模板参数,它可以接收多种数据类型,又称模板参数包。vair_fun() 函数中,args 参数的类型用 T… 表示,表示 args 参数可以接收任意个参数,又称函数参数包

这也就意味着,此函数模板最终实例化出的 vair_fun() 函数可以指定任意类型、任意数量的参数。例如,我们可以这样使用这个函数模板:

vair_fun();
vair_fun(1, "abc");
vair_fun(1, "abc", 1.23);

使用可变参数模板的难点在于,如何在模板函数内部“解开”参数包(使用包内的数据),这里给大家介绍两种简单的方法。

【递归方式解包】
先看一个实例:

#include <iostream>
using namespace std;
//模板函数递归的出口
void vir_fun() {

}

template <typename T, typename... args>
void vir_fun(T argc, args... argv)
{
    cout << argc << endl;
    //开始递归,将第一个参数外的 argv 参数包重新传递给 vir_fun
    vir_fun(argv...);
}

int main()
{
    vir_fun(1, "http://www.biancheng.net", 2.34);
    return 0;
}

执行结果为:

1
http://www.biancheng.net
2.34

分析一个程序的执行流程:

  • 首先,main() 函数调用 vir_fun() 模板函数时,根据所传实参的值,可以很轻易地判断出模板参数 T 的类型为 int,函数参数 argc 的值为 1,剩余的模板参数和函数参数都分别位于 args 和 argv 中;
  • vir_fun() 函数中,首先输出了 argc 的值(为 1),然后重复调用自身,同时将函数参数包 argv 中的数据作为实参传递给形参 argc 和 argv;
  • 再次执行 vir_fun() 函数,此时模板参数 T 的类型为 char*,输出 argc 的值为 "http:www.biancheng.net"。再次调用自身,继续将 argv 包中的数据作为实参;
  • 再次执行 vir_fun() 函数,此时模板参数 T 的类型为 double,输出 argc 的值为 2.34。再次调用自身,将空的 argv 包作为实参;
  • 由于 argv 包没有数据,此时会调用无任何形参、函数体为空的 vir_fun() 函数,最终执行结束。

以递归方式解包,一定要设置递归结束的出口。例如本例中,无形参、函数体为空的 vir_fun() 函数就是递归结束的出口。

【非递归方法解包】
借助逗号表达式和初始化列表,也可以解开参数包。

以 vir_fun() 函数为例,下面程序演示了非递归方法解包的过程:

#include <iostream>
using namespace std;

template <typename T>
void dispaly(T t) {
    cout << t << endl;
}

template <typename... args>
void vir_fun(args... argv)
{
    //逗号表达式+初始化列表
    int arr[] = { (dispaly(argv),0)... };
}

int main()
{
    vir_fun(1, "http://www.biancheng.net", 2.34);
    return 0;
}

这里重点分析一下第 13 行代码,我们以{ }初始化列表的方式对数组 arr 进行了初始化, (display(argv),0)… 会依次展开为  (display(1),0)、(display("http://www.biancheng.net"),0) 和 (display(2.34),0)。也就是说,第 13 行代码和如下代码是等价的:

 int arr[] = { (dispaly(1),0), (dispaly("http://www.biancheng.net"),0), (dispaly(2.34),0) };

可以看到,每个元素都是一个逗号表达式,以 (display(1), 0) 为例,它会先计算 display(1),然后将 0 作为整个表达式的值返回给数组,因此 arr 数组最终存储的都是 0。arr 数组纯粹是为了将参数包展开,没有发挥其它作用。

2) 可变参数类模板

C++11 标准中,类模板中的模板参数也可以是一个可变参数。C++ 11 标准提供的 typle 元组类就是一个典型的可变参数模板类,它的定义如下:

template <typename... Types>
class tuple;

和固定模板参数的类不同,typle 模板类实例化时,可以接收任意数量、任意类型的模板参数,例如:

std:tuple<> tp0;
std::tuple<int> tp1 = std::make_tuple(1);
std::tuple<int, double> tp2 = std::make_tuple(1, 2.34);
std::tuple<int, double, string> tp3 = std::make_tuple(1, 2.34, "http://www.biancheng.net");

如下代码展示了一个支持可变参数的类模板:

#include <iostream>
//声明模板类demo
template<typename... Values> class demo;
//继承式递归的出口
template<> class demo<> {};
//以继承的方式解包
template<typename Head, typename... Tail>
class demo<Head, Tail...>
    : private demo<Tail...>
{
public:
    demo(Head v, Tail... vtail) : m_head(v), demo<Tail...>(vtail...) {
        dis_head();
    }
    void dis_head() { std::cout << m_head << std::endl; }

protected:
    Head m_head;
};

int main() {
    demo<int, float, std::string> t(1, 2.34, "http://www.biancheng.net");
    return 0;
}

程序中,demo 模板参数中的 Tail 就是一个参数包,解包的方式是以“递归+继承”的方式实现的。具体来讲,demo<Head, Tail…> 类实例化时,由于其继承自 demo<Tail…> 类,因此父类也会实例化,一直递归至 Tail 参数包为空,此时会调用模板参数列表为空的 demo 模板类。

程序的输出结果为:

http://www.biancheng.net
2.34
1

可变参数模板类还有其它的解包方法,这里不再一一赘述,感兴趣的读者可以自行做深入的研究。

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/21435.html

(0)
上一篇 2021年7月20日
下一篇 2021年7月20日

相关推荐

发表回复

登录后才能评论