前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >整理C/C++的可变参数

整理C/C++的可变参数

原创
作者头像
Rock_Lee
修改2020-09-23 15:14:28
5.2K0
修改2020-09-23 15:14:28
举报
文章被收录于专栏:知识碎片知识碎片

C语言可变参数

C函数可变参数

c语言中使用可变参数最熟悉应该就是printf, 其是通过...来从代码语句中表示可变化的参数表。

代码语言:txt
复制
void printf(const char* format, ...);

但是这种可变参数最早只能应用在真正的函数中,不能使用在宏中。

直到C99编译器标准,它允许可以定义可变参数宏(variadic macros)

C宏可变参数

可变参数宏:

代码语言:txt
复制
#define DEBUG(...) printf(__VA_ARGS__)

/* 在1999年版本的ISO C 标准中 */
#define LOG(format, ...) fprintf (stderr, format, __VA_ARGS__)

...代表一个变化的参数表

__VA_ARGS__用来把参数传递给宏,当宏被调用展开时,实际的参数就传递给了printf().

在ISO C的版本中,你不能省略可变参数,但是你却可以给它传递一个空的参数。

例如,下面的宏调用在ISO C里是非法的,因为字符串后面没有逗号,

代码语言:txt
复制
LOG("A message")

虽然在GNU CPP中这种情况可以让你完全的忽略可变参数。但是在上面的例子中,编译器仍存在问题,因为宏被展开后,里面的字符串后面会多一个逗号,

为了解决这个逗号,问题,CPP提供一个特殊的##操作,其格式为:

代码语言:txt
复制
#define LOG(format, ...) fprintf(stderr, format, ## __VA_ARGS__)

如果传入的可变参数被忽略或者为空时,##操作会将使得预处理器(preprocessor)去除掉它前面的逗号。

如果传入的可变参数存在,则会按正常工作。

可变参数进行调试

调试的方式输出有很多种,但是标准的方式打印一般不是很方便,于是就可以采用可变参数进行造轮子。

比如我当前的模块名为moduleName,我就可以使用一个包含模块名、文件名、代码行号、函数名等来进行输出调试信息。

代码语言:txt
复制
//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语言中这个头文件

代码语言:txt
复制
#include <stdarg.h>     /* va_list, va_start, va_arg, va_end */

va_arg:宏定义,用来获取下一个参数

va_start:宏定义,开始使用可变参数列表

va_end:宏定义,结束使用可变参数列表

va_list:类型,存储可变参数的信息

通过以上这4个类型,可以进行自定义可变参数的输出函数

代码语言:txt
复制
#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:

代码语言:txt
复制
3,22,444,111,

等等,这里只是实现了参数的可变,参数类型如何可变呢?

其实也可以,注意va_arg宏的第二个参数int了吗?这种硬编码限制了目前我们只能传递int类型。

所以我们可以简陋的添加不同的类型参数进行改造:

代码语言:txt
复制
#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:

代码语言:txt
复制
3,22,444,111,wow,

经过上面的操作,目前可以初步实现参数的数量和参数类型的可变。并且可以发现printf的实现为什么一定需要%s,%d等这种格式化字符串是为了给va_*宏两点关键信息:1.可变参数的个数(百分号的个数);2.可变参数的类型(%s,%d等)

不过C++作为扩展C,当然克服了这些限制。

于是C++提供了可变参数模板

C++可变参数

C++的可变参数模板是怎么做到不需要告诉参数个数的呢?它仰仗以下的功能:

1.函数重载,依靠参数的pattern去匹配对应的函数;

2.函数模板,依靠调用时传递的参数自动推导出模板参数的类型;

3.类模板,基于partial specialization来选择不同的实现;

C++模板

代码语言:txt
复制
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个类型作简单解释:

  1. typename... Ts,这是template parameter pack,表明这里有多种type;
  2. Ts... arg_left,这是function parameter pack,表明这里有多个参数;
  3. arg_left...,这是pack expansion,将参数名字展开为逗号分割的参数列表;

具体的:

第一步:

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的“非模板重载”版本,目的就是为了递归能够最终退出,基于这个原理,我们也可以按照如下方式重新实现:

代码语言:txt
复制
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”的版本。

sizeof... 操作符

根据上面的定义可以看出相比C语言的可变参数VA_*要好使,但是还有一点比较麻烦:模板函数总是需要定义两次,目的是为了让递归退出。那是否有更优雅的方法呢?

C++11中引入了sizeof...操作符,可以得到可变参数的个数,注意sizeof...的参数只能是parameter pack

例如

代码语言:txt
复制
std::cout<<sizeof...(Ts)<<std::endl;
std::cout<<sizeof...(arg_left)<<std::endl;

这样我们就可以通过sizeof...来判断当parameter pack的个数为零时,退出递归。

代码语言:txt
复制
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");
}

但是不幸的是这样程序会报错

代码语言:txt
复制
error: no matching function for call to ‘newPrint()’
newPrint(arg_left...);

这是因为,可变参数模板newPrint的所有分支都被实例化(instantiation),并不会考虑上面那个if表达式。一个instantiated的代码是否有用是在runtime时决定的,而所有的instantiation是在编译时决定的。所以newPrint()空参数版本照样被instandiated,而当instandiated的时候并没有发现对应的实现,于是编译期报错。

C++17的if constexpr表达式和梦想实现

C++17中引入了编译期if表达式if constexpr,可以用来完美的解决这个问题:

代码语言:txt
复制
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");
}

参考文档

C++的可变参数模板

C/C++可变参数,“## VA_ARGS”宏的介绍和使用

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • C语言可变参数
    • C函数可变参数
      • C宏可变参数
        • 可变参数进行调试
          • 深入理解
          • C++可变参数
            • C++模板
              • 换个花样重载
              • sizeof... 操作符
            • C++17的if constexpr表达式和梦想实现
            • 参考文档
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档