专栏首页用户7225096的专栏整理C/C++的可变参数
原创

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

C语言可变参数

C函数可变参数

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

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

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

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

C宏可变参数

可变参数宏:

#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++可变参数

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

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

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

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

C++模板

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

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

例如

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 constexpr表达式和梦想实现

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");
}

参考文档

C++的可变参数模板

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

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

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

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Determining 32 vs 64 bit in C++

    2.size of pointer 通常情况下,在32位平台上一个指针的宽度为4bytes,而在64位平台上位8bytes.

    Rock_Lee
  • pImpl

    在C ++中,如果头文件类定义中的任何内容发生更改,则必须重新编译该类的,即使所更改是私有类成员。这是因为C ++的构建模型基于文本包含(textual inc...

    Rock_Lee
  • 运行时类型识别

    运行时类型识别(run-time type identification, RTTI)的功能由两个运算符实现:

    Rock_Lee
  • 高阶实战 | 如何用Python检测伪造的视频

    译者注:本文以一段自打24小时耳光的视频为例子,介绍了如何利用均值哈希算法来检查重复视频帧。以下是译文。 有人在网上上传了一段视频,他打了自己24个小时的耳光。...

    小小科
  • Swoole 2020 :4.5 新版本的规划

    转眼 Swoole 开源项目已经历 8 个年头。这 8 年里,有 116 位开发者为 Swoole 贡献了内核代码。有无数 PHP 开发者为 Swoole 提供...

    桶哥
  • Mysql 常用函数(36)- monthname 函数

    https://www.cnblogs.com/poloyy/p/12890763.html

    小菠萝测试笔记
  • 女博士用3D打印“卧底”海龟蛋,装GPS骗过偷猎者,还揭发了137公里的非法贸易链

    3D打印再次刷新了文摘菌的认知,不仅可以打印房子、打印“机器狗”,打印疫情期间能救命的呼吸阀零件,用3D打印出的东西还能以假乱真当回“卧底”。

    大数据文摘
  • 可能是全站最完整的Redis分布式锁架构演进

    已经拿到了 lockvalue ,有了 UUID,但是过期了现在!其他人拿到所锁设置了新值,于是 if 后将别人的锁删了!!也就是删除锁不是原子操作。

    JavaEdge
  • Python填充任意颜色,不同算法时间差异分析说明

    以上这篇Python填充任意颜色,不同算法时间差异分析说明就是小编分享给大家的全部内容了,希望能给大家一个参考。

    砸漏
  • jQuery基础--基本概念

    js库:把一些常用到的方法写到一个单独的js文件,使用的时候直接去引用这js文件就可以了。(animate.js、common.js)

    eadela

扫码关注云+社区

领取腾讯云代金券