C/C++拾遗之含有可变形参的函数

C++11中提供了两种可以实现可变参数的函数实现方式:initializer_list的标准模板类型和可变参数模板。C语言中可以通过省略符类型实现可变数量的参数函数(推荐只有在需要与C函数交互时才使用些方式)。

initializer_list形参

C++11中的头文件initializer_list中提供了一种标准库类型initializer_list类型,通过该模板可以实现全部实参类型都相同的可变参数函数。使用时只需要将initializer_list类型的参数作为函数形参即可,因为initializer_list可以接受多个元素作为初始化列表,相当于一个变长数组,然后在函数中通过initializer_list头文件中提供的相应函数获取其中的元素即可。initializer_list提供的操作有:

  • initializer_list lst; //默认初始化;T类型元素的空列表
  • initializer_list lst{a, b, c…}; lst的元素是相应初始值的副本,且列表中的元素是const
  • lst2(lst); lst2 = lst; 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素
  • lst.size() 列表中的元素数量
  • lst.begin() 返回指向lst中首元素的指针
  • lst.end() 返回指向lst中尾元素下一位置的指针

用法举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

#include <initializer_list>
#include <iostream>

template <typename T> T sum(std::initializer_list<T> args)
{
T total = 0.0;
for (const auto &item : args) //使用范围for
total += item;
return total;
}

int main(void)
{
std::cout << sum<int>({1, 2, 3, 4, 5}) << std::endl;
std::cout << sum<double>({1, 1.2, 3, 14.4}) << std::endl;
return 0;
}

输出为:

1
2
15
19.6

与我们期望的一样,double类型的运算中,整型也被正确地隐式转换为了浮点数参与计算

可变参数模板

一个可变参数模板就是一个可以接受可变数目参数的模板函数或模板类,当然这里我们只会用到模板函数。可变数目的参数被称为参数包,包括模板参数包(由0个多或多个类型组成)和函数参数包(由0个或多个函数参数组成)。可变参数的函数模板声明通常形如:

1
2
3
//Args是一个模板参数包,rest是一个函数参数包
template <typename T, typename ... Args>
void func(const T &t, const Args& ... rest); //扩展Args

也就是typename 省略号(...) 模板参数包名字来定义一个模板参数包,扩展模式 模板包名字 省略号(...) 函数参数包名字来将模板包扩展成相应模式下的函数参数包,同样当将函数参数包名字后跟省略号时,即可扩展函数参数包。当编译器在对模板特例化时,会自动按照提供给参数包的模式进行扩展包,扩展之后的参数相当于一个以逗号分隔的参数列表,如上例中const Arg&会应用到模板参数包Args中的每一个元素,也就是最终rest中包含的所有元素都将是const的引用,类型由编译器根据传递的参数进行自动推断。
注意可变参数模板的使用不需要再指定类型,具体类型由编译器根据传递给函数的实参进行推断。
当需要在函数内部访问各参数列表中的参数时,直接对函数参数包进行扩展即可,通常是采用递归的方式访问各参数,为了终止递归就需要定义一个重载的函数用于终止递归
与可变参数模板对应的还有一个sizeof…运算符,用于返回参数包中的参数个数,当然可以用它来配合递归函数访问各参数。
下面还是以一个计算和的例子分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>

template <typename T, typename... Args>
void foo(const T &t, const Args& ... args)
{
std::cout << "sizeof...(Args): " << sizeof...(Args) << std::endl; //输出模板参数Args的个数
std::cout << "sizeof...(args): " << sizeof...(args) << std::endl; //输出函数参数args的个数
}

template <typename T>
std::ostream& print(std::ostream &os, const T &t) //用于结束递归,当参数个数为2个时调用
{
return os << t << std::endl;;
}

template <typename T, typename ... Args>
std::ostream& print(std::ostream &os, const T &t, Args ... rest) //当参数个数大于2个时调用
{
os << t << ", ";
return print(os, rest...);
}

template <typename T>
T sum_template(const T &t) //声明一个重载的固定参数数目的函数,用于结束递归
{
return t;
}

template <typename T, typename... Args>
T sum_template(const T &t, const Args&... args) //该函数通过调用仅含一个参数的重载函数来结束递归
{
return sum_template(args...) + t; //展开函数参数包args中的实参
}


int main(void)
{
foo('a', 2, 3, "ssl");
print(std::cout, 'a', 2, 3, "ssl");
std::cout << sum_template(1, 2, 3, 4, 5) << std::endl;
std::cout << sum_template(1.1, 2.5, 3, 4.4, 5) << std::endl;
return 0;
}

输出结果为:

1
2
3
4
5
sizeof...(Args): 3
sizeof...(args): 3
a, 2, 3, ssl
15
15.6

结果后面的求和输出结果可见,对于将double与int混合计算时结果是错误的,因为计算过程中发生了double到int的转换,导致精度损失,所以对于这种情况要注意类型转换问题。

省略符类型(…)

C语言中通过头文件stdarg.h提供的几个宏定义可以实现可变参数的函数,但要求该函数至少要有一个确定的类型的参数。对应C++中的头文件是cstdarg,C++中也允许只有省略参数类型的函数。
详细参考链接http://www.cse.unt.edu/~donr/courses/4410/NOTES/stdarg/
主要需要使用到cstdarg头文件中的1个类型和3个函数:

  • va_list varname 用于定义指向参数列表的变量varname
  • va_start(varname, last_defined_arg) 用于将varname指向所有参数中最后一个确定的参数
  • va_arg(varname, typename) 获取下一个类型为typename的参数
  • va_end(varname) 在函数返回前调用,用于清空参数列表栈,以使函数可以正常返回
  • va_copy(varname) 拷贝参数列表(C++11)
    举例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include <cstdarg>
    #include <iostream>

    //切勿混合使用cstdarg与模板
    double sum_arg(int argnum, ...) //至少要有一个确定的参数,通常第一个参数用于传递参数的个数
    {
    double total = 0.0;
    va_list args; //定义一个参数列表变量
    va_start(args, argnum); //初始化参数列表变量,指向最后一个确定的参数(因为可以有多个确定的参数)
    for (int i = 0; i < argnum; ++i)
    total += va_arg(args, double); //获取下一个参数
    va_end(args); //清空系统栈,返回使用函数调用者可以正常返回
    return total;
    }

    int main(void)
    {
    std::cout << "double-1: " << sum_arg(5, 1, 2, 3.4, 5, 6.2) << std::endl;
    std::cout << "double-2: " << sum_arg(5, 1.0, 2.0, 3.4, 5.0, 6.2) << std::endl;
    return 0;
    }

输出为:

1
2
double-1: 1.36621e+161
double-2: 17.6

发现double-1的输出结果是错误的,因为double-1中传递的有一些整型值,此时函数中只会读取参数列表中所有的浮点型,一共只有2个浮点型,导致后面的3个参数读取的内存中存储随机值,所以出错。而double-2中都是浮点数,所以没有问题。可见这种方式不像initializer_list的方式那样可以进行隐式类型转换。