C++中三种实现可变参数的方法

我真的只是想写一个log函数而已。


可变参数宏(C实现)

利用头文件stdarg.h中的宏定义:va_listva_start(va_list, arg)va_arg(va_list, type)va_end(va_list)。可以实现可变参数且参数类型不同的函数。

过程:

  • 函数声明中,可变参数用省略号表示。
  • 创建va_list变量,通过va_start访问参数列表并承接第一个参数。
  • 使用va_arg继续获取参数的值。
  • 使用va_end完成清理工作。

以可变参数版本的sum作为例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdarg.h>
#include <stdio.h>
int sum(int count, ...) //count 表示可变参数个数
{
va_list ap; //声明一个va_list变量
va_start(ap, count); //初始化,第二个参数为最后一个确定的形参(不能另开局部变量)
int sum = 0;
for (int i = 0; i < count; i++)
sum += va_arg(ap, int); //读取可变参数,第二个参数为可变参数的类型
va_end(ap); //清理工作
return sum;
}
int main()
{
printf("%d\n", sum(5, 1, 2, 3, 4, 5));
return 0;
}

局限性:

  • 可变参数宏只能实现顺序访问可变参数,无法后退访问。但可以重复使用va_start来初始化va_list变量。
  • 运行时,函数必须能够根据已有信息(既有约定,或确定实参)确定可变参数的具体个数与类型:函数定义需要知道可变参数的具体类型、个数,这些信息是在运行时确定的,那么显然应该由实参来确定。在上面的例子中count传递了可变参数的个数,而参数类型则是既有约定(整型);
  • 该方法是极不安全的,宏本身无法提供任何安全性保证,他总是按照既定代码“自作多情”的认为实参就应该是那么多,即使实参并不是那么多。这就要求所有安全性必须由程序员来保证。例如,在以上的示例代码中,如果调用时指定count为10,但实际上只给出9个可变形参,那么函数还是会读取10个参数,显然第十次读取是多余的,多余的操作一般不会有什么好结果,当然如果实参过多,多余的实参也不会被读取而是被忽略。

initializer_list标准库类型(C++11)

过程:

  • 函数声明中使用initializer_list模板代表可变参数列表
  • 使用迭代器访问initializer_list中的参数
  • 传入参数时需要使用{}把多个参数括起来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <initializer_list>
#include <iostream>
int sum(std::initializer_list<int> il)
{
int sum = 0;
for (auto p : il) // 使用范围for
sum += p;
// for (auto p = il.begin(); p != il.end(); p++) //使用迭代器访问参数
// sum += *p;
return sum;
}
int main()
{
std::cout << sum({1, 2, 3, 4, 5}) << std::endl;
return 0;
}

局限性:

  • 可变参数类型必须一致

可变参数模板(C++11)

更方便安全有效地实现可变参数且参数类型不同的函数。

过程:

  • 编写含有模板参数包和函数参数包的模板函数
  • 函数定义递归调用自己
  • 利用函数重载(参数包含有零个参数)来处理边界情况,编写处理边界情况的模板

C++ primer 第五版中的例子:

1
2
3
4
5
6
7
8
9
10
11
12
// 用来终止递归并打印最后一个元素的函数
// 此函数必须在可变参数版本的print定义之前声明(否则将出现neither visible nor found by argument-dependent lookup错误)
template <typename T>
std::ostream &print(std::ostream &os, const T &t){
return os << t; // 包中最后一个元素
}
//包中除最后一个元素之外的其他元素都会调用这个版本的pirnt
template <typename T, typename... Args>
std::ostream &print(std::ostream &os, const T &t, cosnt Args &... rest){
os << t << ","; // 打印第一个实参,包中元素减一
return print(os, rest...); // 递归调用,打印剩余实参
}

局限性:

  • 由于只能递归实现,导致需要定义重载函数来处理边界情况,代码不够清晰自然。
  • 模板会为每一个不同的实例生成代码,函数的实例过多可能会使代码体积庞大。
  • 依靠递归使得功能具有局限性,并且效率也会受到影响。

log函数实现

头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* log.h */
#ifndef LOG_H_
#define LOG_H_
#include <iostream>
std::ostream &log() { return std::cout; }
template <typename T, typename... Args>
std::ostream &log(T const &p, Args const &... rest)
{
std::cout << p;
return log(rest...);
}
#endif

测试代码:

1
2
3
4
5
6
7
8
9
10
/* test.cpp */
#include "log.h"
int main()
{
int n = 2;
log(n, " hello ", 10086) << std::endl; // std::cout << "2 hello 10086" << std::endl;
return 0;
}