c语言中使用可变参数最熟悉应该就是printf
, 其是通过...
来从代码语句中表示可变化的参数表。
void printf(const char* format, ...);
但是这种可变参数最早只能应用在真正的函数中,不能使用在宏中。
直到C99编译器标准,它允许可以定义可变参数宏(variadic macros)
可变参数宏:
#define DEBUG(...) printf(__VA_ARGS__)
/* 在1999年版本的ISO C 标准中 */
#define LOG(format, ...) fprintf (stderr, format, __VA_ARGS__)
...
代表一个变化的参数表
__VA_ARGS__
用来把参数传递给宏,当宏被调用展开时,实际的参数就传递给了printf()
.
在ISO C的版本中,你不能省略可变参数,但是你却可以给它传递一个空的参数。
例如,下面的宏调用在ISO C里是非法的,因为字符串后面没有逗号,
:
LOG("A message")
虽然在GNU CPP中这种情况可以让你完全的忽略可变参数。但是在上面的例子中,编译器仍存在问题,因为宏被展开后,里面的字符串后面会多一个逗号,
为了解决这个逗号,
问题,CPP提供一个特殊的##
操作,其格式为:
#define LOG(format, ...) fprintf(stderr, format, ## __VA_ARGS__)
如果传入的可变参数被忽略或者为空时,##
操作会将使得预处理器(preprocessor)去除掉它前面的逗号。
如果传入的可变参数存在,则会按正常工作。
调试的方式输出有很多种,但是标准的方式打印一般不是很方便,于是就可以采用可变参数进行造轮子。
比如我当前的模块名为moduleName
,我就可以使用一个包含模块名、文件名、代码行号、函数名等来进行输出调试信息。
//debug.c
#include <stdio.h>
#include <string.h>
#define _DEBUG
#ifdef _DEBUG
//开启下面的宏就把调试信息输出到文件,注释即输出到终端
#define DEBUG_TO_FILE
#ifdef DEBUG_TO_FILE
//调试信息输出到以下文件
#define DEBUG_FILE "/tmp/debugmsg"
//调试信息的缓冲长度
#define DEBUG_BUFFER_MAX 4096
//将调试信息输出到文件中
#define printDebugMsg(moduleName, format, ...) {\
char buffer[DEBUG_BUFFER_MAX+1]={0};\
snprintf( buffer, DEBUG_BUFFER_MAX \
, "[%s] "format" File:%s, Line:%d\n", moduleName, ##__VA_ARGS__, __FILE__, __LINE__ );\
FILE* fd = fopen(DEBUG_FILE, "a");\
if ( fd != NULL ) {\
fwrite( buffer, strlen(buffer), 1, fd );\
fflush( fd );\
fclose( fd );\
}\
}
#else
//将调试信息输出到终端
#define printDebugMsg(moduleName, format, ...) \
printf( "[%s] "format" File:%s, Line:%d\n", moduleName, ##__VA_ARGS__, __FILE__, __LINE__ );
#endif //end for #ifdef DEBUG_TO_FILE
#else
//发行版本,什么也不做
#define printDebugMsg(moduleName, format, ...)
#endif //end for #ifdef _DEBUG
1) FILE 宏在预编译时会替换成当前的源文件名
2) LINE宏在预编译时会替换成当前的行号
3) FUNCTION宏在预编译时会替换成当前的函数名称
上面我们讨论了printf
带来的可变参数。
这里的可变主要指两点可变:
1.参数数量可变
2.参数类型可变
具体的实现主要是借助于C语言中这个头文件
#include <stdarg.h> /* va_list, va_start, va_arg, va_end */
va_arg
:宏定义,用来获取下一个参数
va_start
:宏定义,开始使用可变参数列表
va_end
:宏定义,结束使用可变参数列表
va_list
:类型,存储可变参数的信息
通过以上这4个类型,可以进行自定义可变参数的输出函数
#include <cstdarg>
void diyPrint(int n, ...){
va_list args;
va_start(args, n);
while(n--){
std::cout<<va_arg(args, int)<<", ";
}
va_end(args);
std::cout<<std::endl;
}
int main(int argc, char** argv)
{
diyPrint(3, 22,444,111);
}
Output:
3,22,444,111,
等等,这里只是实现了参数的可变,参数类型如何可变呢?
其实也可以,注意va_arg宏的第二个参数int了吗?这种硬编码限制了目前我们只能传递int类型。
所以我们可以简陋的添加不同的类型参数进行改造:
#include <cstdarg>
void diyPrint(int n, ...){
va_list args;
va_start(args, n);
while(n--)
{
if(n == 0)
{
std::cout<<va_arg(args, const char*)<<", ";
continue;
}
std::cout<<va_arg(args, int)<<", ";
}
va_end(args);
std::cout<<std::endl;
}
int main(int argc, char** argv)
{
diyPrint(3, 22,444,111,"wow");
}
Output:
3,22,444,111,wow,
经过上面的操作,目前可以初步实现参数的数量和参数类型的可变。并且可以发现printf
的实现为什么一定需要%s,%d
等这种格式化字符串是为了给va_*
宏两点关键信息:1.可变参数的个数(百分号的个数);2.可变参数的类型(%s,%d等)
不过C++作为扩展C,当然克服了这些限制。
于是C++提供了可变参数模板
C++的可变参数模板是怎么做到不需要告诉参数个数的呢?它仰仗以下的功能:
1.函数重载,依靠参数的pattern去匹配对应的函数;
2.函数模板,依靠调用时传递的参数自动推导出模板参数的类型;
3.类模板,基于partial specialization来选择不同的实现;
void newPrint(){std::cout<<std::endl;}
template<typename T, typename... Ts>
void newPrint(T arg1, Ts... arg_left){
std::cout<<arg1<<", ";
newPrint(arg_left...);
}
int main(int argc, char** argv)
{
newPrint(1,22,"wow");
}
看起来比C的实现要简单多了,上面3个类型作简单解释:
具体的:
第一步:
main函数里调用了newPrint(1,22,"wow");会导致newPrint函数模板首先展开为:
void newPrint(int, int, const char*)
第二步:
在打印第1个参数1后,newPrint递归调用了自己,传递的参数为arg_left...
,该参数会展开为【22,"wow"】,newPrint第2次进行了展开:
void newPrint(int, const char*)
第三步:
在打印第2个参数22,newPrint递归调用了自己,传递的参数为arg_left...
,该参数会展开为【"wow"】,newPrint第3次进行了展开:
void newPrint(const char*)
第四步:
在打印第3个参数"wow"后,newPrint递归调用了自己,传递的参数为arg_left...
,该参数会展开为【】,newPrint准备进行第4次展开:
void newPrint()
但是,我们已经定义了这个函数:
void newPrint()(){std::cout<<std::endl;}
上面这个函数是函数模板newPrint()的“非模板重载”版本,于是展开停止,直接调用这个“非模板重载”版本,递归停止。
上面的例子里有个newPrint的“非模板重载”版本,目的就是为了递归能够最终退出,基于这个原理,我们也可以按照如下方式重新实现:
template<typename T>
void newPrint(T arg){std::cout<<arg<<", "<<std::endl;}
template<typename T, typename... Ts>
void newPrint(T arg1, Ts... arg_left){
std::cout<<arg1<<", ";
newPrint(arg_left...);
}
int main(int argc, char** argv)
{
newPrint(1,22,"wow");
}
这里是两个函数模板,区别是模板参数的区别:当两个参数模板都适用某种情况时,优先使用没有“template parameter pack”的版本。
根据上面的定义可以看出相比C语言的可变参数VA_*
要好使,但是还有一点比较麻烦:模板函数总是需要定义两次,目的是为了让递归退出。那是否有更优雅的方法呢?
C++11中引入了sizeof...
操作符,可以得到可变参数的个数,注意sizeof...
的参数只能是parameter pack
例如
std::cout<<sizeof...(Ts)<<std::endl;
std::cout<<sizeof...(arg_left)<<std::endl;
这样我们就可以通过sizeof...
来判断当parameter pack的个数为零时,退出递归。
template<typename T, typename... Ts>
void newPrint(T arg1, Ts... arg_left){
std::cout<<arg1<<", ";
if(sizeof...(arg_left) > 0){
newPrint(arg_left...);
}
}
int main(int argc, char** argv)
{
newPrint(1,22,"wow");
}
但是不幸的是这样程序会报错
error: no matching function for call to ‘newPrint()’
newPrint(arg_left...);
这是因为,可变参数模板newPrint
的所有分支都被实例化(instantiation),并不会考虑上面那个if表达式。一个instantiated的代码是否有用是在runtime时决定的,而所有的instantiation是在编译时决定的。所以newPrint()空参数版本照样被instandiated,而当instandiated的时候并没有发现对应的实现,于是编译期报错。
C++17中引入了编译期if表达式if constexpr
,可以用来完美的解决这个问题:
template<typename T, typename... Ts>
void newPrint(T arg1, Ts... arg_left){
std::cout<<arg1<<", ";
if constexpr(sizeof...(arg_left) > 0){
newPrint(arg_left...);
}
}
int main(int argc, char** argv)
{
newPrint(1,22,"wow");
}
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。