C/C++常见的变长参数技巧包括变长模板、变长函数参数和变长宏参数。

变参数函数

最常见的变参数函数就是printfscanf之类的,利用stdarg.h对变参数函数支持实现的变参函数。其基本思路是根据格式串来判断后面的参数类别,例如读到%d,那么下一个参数就是int/long int,且按十进制整数的方式处理。
下面是一个丐版printf函数的示例,只处理了单字符、单字符串、双精度浮点数和十进制整数4种情况。实际上使用的printf很复杂,在glibc中的vfprintf函数就有2300行源码,包括了对各类基本数据类型输出的支持,还有对齐、填充、浮点样式等支持。

#include<stdio.h>
#include<stdarg.h>void myprintf(const char* fmt, ...) {int symf = 0;va_list args;va_start(args, fmt);int va_int;double va_flt;const char* va_str;char va_chr;while (*fmt != '\0') {if (symf) {if (*fmt == 'c')va_chr = va_arg(args, char), printf("%c", va_chr);else if (*fmt == 's')va_str = va_arg(args, const char*), printf("%s", va_str);else if (*fmt == 'd')va_int = va_arg(args, int), printf("%d", va_int);else if (*fmt == 'f')va_flt = va_arg(args, double), printf("%lf", va_flt);symf = 0;} else {if (*fmt == '%')symf = 1;elseputchar(*fmt);}fmt++;}putchar('\n');va_end(args);
}

通常的变参函数都有如下格式:

int my_variadic_func(int argc, ...) {// initializeva_list args;va_start(args, argc);int res = 0;/* deal with variadic arguments */va_end(args);// return the answerreturn res;
}

C23标准前的变参函数支持要求,所有变参函数都至少有一个不变参数,从这个参数开始,所有参数视作变参数。
例如:

int printf(const char* fmt, ...);void foo()
{...printf("%d %d %d\n", 2, 3, 5);...
}

printf的定义中,fmt作为不变参数,其后的所有参数视作变参数,包括将被按格式输出的各个表达式参数。
变参函数如何访问和使用变参数?va_list结构是C语言标准中用于支持变参函数访问的结构,它允许用户用va_start宏设置变参数的起始位置,用va_arg逐个访问变参数,用va_end结束对变参数的访问。
在上面的变参函数格式my_variadic_func中,va_list args创建了一个变参列表结构,下一行的va_start(args, argc)指定变参数从argc的下一个参数开始,而va_end(args)表示对变参数的访问结束。
至于中间访问变参数,要使用va_arg宏,它的格式是va_arg(vl, type)vl表示被访问的变参列表,type表示想取的参数类型。具体而言,是调用一次就取走一个参数,下一次调用就取走下一个。注意,取走的参数数量没有明确限制,如果超出变参范围继续取,就会越界访问参数后面的用户栈,所以必须在实现函数时做好约定,让调用者正确地调用函数,在函数实现中取走正确个数的参数,否则会造成程序混乱。printf等输出函数是通过fmt串约定的变参个数,具体实现上,你也可以通过一个整数参数表示变参个数。
完善上面的my_variadic_func,使其成为一个求和函数:

int my_variadic_func(int argc, ...) {va_list args;va_start(args, argc);int res = 0, x;while (argc--) {x = va_arg(args, int);res += x;}va_end(args);return res;
}

另外,va_copy支持完全复制一个现有的变参列表,在变参函数传参时会很有用。详情参考:C++ Reference: va_copy

变参数宏

变参数宏是指支持可变个数参数的文本替换宏#define,这个预处理语法最早在C++11被支持。它允许使用__VA_ARGS__访问参数列表里...所指的内容,也允许将变参数加前缀命名,例如args...etc...,也可以起到__VA_ARGS__一样的效果。

#define myprintf(...)                                                  \do {                                                                 \time_t lt = time(0);                                               \tm* localt = localtime(&lt);                                       \printf("[%04d-%02d-%02d %02d:%02d:%02d] ", localt->tm_year + 1900, \localt->tm_mon + 1, localt->tm_mday, localt->tm_hour,       \localt->tm_min, localt->tm_sec);                            \printf("[%12s:%4d] ", __FILE__, __LINE__);                         \printf(__VA_ARGS__);                                               \putchar('\n');                                                     \} while (0);int main() {myprintf("ALOHA");return 0;
}

上面是一个按日志格式输出格式化文本的示例。因为我不太懂怎么在两个变参数函数之间传变参,所以我一般用宏的方式处理。
还有一个需要注意的点就是,对于带定参数的可变参数宏(例如macprintf(fmt,...)),可能出现因为没传可变参数导致__VA_ARGS__实际为空,间接导致编译错误的问题。在C++20之前的GCC编译器下,解决方法是用##预处理运算符,将预处理定义修改成诸如下面的格式:

#define mpr(fmt,...) printf(fmt, ##__VA_ARGS__);
#define mpr(fmt,args...) printf(fmt, ##args);

从GNU C++20开始,__VA_OPT__(x)用于在__VA_ARGS__非空时表示内容x,若__VA_ARGS__为空,则不具备意义。

#if __cplusplus >= 202002L // C++20
#define mpr(fmt,...) printf(fmt __VA_OPT__(,) __VA_ARGS__);
#else
// mpr(fmt,...) of older versions than C++20
#endif

这个定义和上面两个定义是等价的。
注意:对于C++中的宏,原则仍然是能不用就不用,只有在其对降低工作量的效果远胜于其潜在出错成本时才可以用。如果一定要使用基于文本替换的C++宏,若替换内容为表达式,在外加一层圆括号,如果替换内容为语句或语句组,则用一个do-while语句块将其包裹起来。

可变参数模板

虽然我比较讨厌C++模板编程的一些细节,但作为学生还是有机会学的都学一下子。
我平时做算法题经常使用的一个东西就是debug函数,可以不写老臭的#ifndef ONLINE_JUDGE,提交之前将函数体内注释掉就可以。

void debug(){ cout<<endl; }
template<class T1,class... T2>
void debug(T1 a,T2... oth)
{cout<<a<<' ';debug(oth...);
}

上面的debug接受任意个数参数并将这些参数输出到标准输出流。准确来讲,这种语法叫做形参包,是接受零个或更多个模板实参(非类型、类型或模板)的模板形参。
C++20之前,支持这三类形参包作为模板形参出现。C++20开始出现带类型约束的模板形参包,这里不做扩展。

// 1 - fixed type
int... args
// 2 - variadic types
class... args
typename... args
// 3 - nested packages
template</* arguments of nested package */>... args

最常用的是第二类,即类型模板形参包,可匹配0或多个模板实参,且对实参类型限制较少。
形参包最常见的作用,就是为模板提供扩充功能,使其不限于固定个数模板参数,这提供了另一种变参数函数的实现方法,以及使得基于可变模板的一些STL黑科技成为可能。
下面是一个利用形参包特性求和的类模板(实际不可能这么写,这个只能实现静态运算,这里只是举个栗子):

template<int arg0 = 0, int... args>
class Sum
{public:int operator()() {if(sizeof...(args) == 0) return arg0;else return arg0 + Sum<args...>()();}
};

具体而言,带形参包的函数模板接受任意不少于固定形参个数的入口参数,上面的debug由于额外重载了空参,可以接受任意多的参数;带形参包的类模板则接受任意不少于固定形参个数的模板实参,像上面的Sum就可以接受任意多个数的int常量。

形参包的一个重要操作是包的展开。包展开依托一个模式,模式则是包含至少一个形参包的类模板,例如我们的形参包为typename... args,则argsstd::vector<args>std::pair<args,int>均为合法模式。如果包含两个及以上形参包,则所有形参包必须等长。这个很绕的例子大概可以说明这个特性:

template <typename... args>
class TypeArray {};
template <typename A, typename B>
class TypePair {};
template <typename... args1>
class TypeMatcher {public:template <typename... args2>class Accepts {public:using result_type = TypeArray<TypePair<args1, args2>...>;};
};// sizeof...(args1) == sizeof...(args2) ===> OK
using T1 = TypeMatcher<int, short>::Accepts<size_t, char16_t>::result_type;
// Compilation error: mismatched argument pack lengths while expanding 'TypePair<args1, args2>'
using T2 = TypeMatcher<int, short>::Accepts<std::string>::result_type;

在模式合法的前提下,包展开的格式为:

pattern...

形参包会原地展开为包内各实参代入模式后的结果序列。例如TypeArray<args>...args=[int,int64_t,__int128_t]展开后即为TypeArray<int>,TypeArray<int64_t>,TypeArray<__int128_t>,而不是有些人可能设想的TypeArray<int,int64_t,__int128_t>。后者实际上是TypeArray<args...>的展开结果。

谨慎在相近位置使用多个包展开。包展开运算具有比较低(低于绝大多数运算,不知道是不是最低)的优先级,所以会在一些情形下造成反常识的展开结果。例如下面的例子:

template<class... args> int h(args... a) { return sizeof...(args); }template<class... Args>
int f(Args... args)
{h(h(args...)+args...);
}f(1,2,3);

很多人会认为前后两个都是单独对args展开,但实际不是。前一个args...展开为1,2,3没有问题,问题在于编译器会认为第一次展开后的h(1,2,3)+args是一个模式,而不是第二个args自身。所以实际展开结果是:h(1,2,3)+1,h(1,2,3)+2,h(1,2,3)+3,而非:h(1,2,3)+1,2,3

另外,ISO C++也对允许包展开的情形进行了十分严格的限制,详情参考:包展开的场所。

目前C++官方提供了这三种变长参数支持。在它们之间选择应当加以权衡,选择最适合的方法。

C/C++:变长参数技巧汇总相关推荐

  1. 变长参数模板 和 外部模板

    变长参数模板 解释 C++03只有固定模板参数.C++11 加入新的表示法,允许任意个数.任意类别的模板参数,不必在定义时将参数的个数固定. 变长模板.变长参数是依靠C++11新引入的参数包的机制实现 ...

  2. 关于c++变长参数列表总结

    2019独角兽企业重金招聘Python工程师标准>>> 写在前面 在C++语言中,有两个三个(???)地方用到了"..."这个符号,分别是: 变长参数列表.下面用 ...

  3. Java变长参数应该注意的问题

    从Java SDK 5开始,Java就支持了变长参数,但是在使用时应该注意如下问题: 1.变长参数只能放在最后. 2.如果出现重载情况,应该注意模糊性. 例如: class VarArgs { sta ...

  4. c语言 宏 变长参数,科学网—C/C++中处理变长参数函数(Variadic Function)的几个宏 - 彭彬的博文...

    近日在模式中进行非线性方程组求解时遇到变长参数函数的问题,以前从来没有自己写过变长参数的函数,于是补了一下课,将近日对该小问题的学习和理解整理如下. 一.变长参数函数(variadic functio ...

  5. c/c++十七: 变长参数

    #include"c1.h" #include<stdarg.h> typedef int ElemType;ElemType Max(int num,...){ // ...

  6. *args and **kwargs in Python 变长参数

    原文链接 变长参数 args(非关键字参数) def myFun(*argv): for arg in argv: print (arg)myFun('Hello', 'Welcome', 'to', ...

  7. Spark UDF变长参数的二三事儿

    在复杂业务逻辑中,我们经常会用到Spark的UDF,当一个UDF需要传入多列的内容并进行处理时,UDF的传参该怎么做呢? 下面通过变长参数引出,逐一介绍三种可行方法以及一些不可行的尝试... 引子 变 ...

  8. java 变长参数 知乎_变长参数探究

    前言 变长参数,指的是函数参数数量可变,或者说函数接受参数的数量可以不固定.实际上,我们最开始学C语言的时候,就用到了这样的函数:printf,它接受任意数量的参数,向终端格式化输出字符串.本文就来探 ...

  9. matlab 变长参数,变长参数函数的概念

    分享一个2015年华为笔试知识点:变长参数函数 变长参数的函数即参数个数可变.参数类型不定 的函数. 设计一个参数个数可变.参数类型不定的函数是可能的,最常见的例子是printf函数.scanf函数和 ...

最新文章

  1. mysql隐式转换造成索引失效的事故总结
  2. pc控制iphone的软件_如何在iPhone上下载升级最新的iOS 13公测版
  3. boost::type_index模块constexpr相关的测试程序
  4. python redis模块connectionerror_PHP程序连接Redis报read error on connection问题
  5. iOS 编写高质量Objective-C代码(六)
  6. python爬取百度域名注册_python爬取百度域名_python爬取百度搜索結果url匯總
  7. 用CSS使DIV水平居中
  8. appium+python 【Mac】Android夜神模拟器
  9. 【洛谷P1256】公路修建(问题分析+最小生成树prim法)
  10. 机器学习和模式识别怎么区分?
  11. 如何防止局域网病毒春风吹又生--之一
  12. 一篇爽文带你全面了解mysql的索引
  13. 计算机在康复治疗学中的应用,等速运动在康复医学中的应用 【康复医学讨论版】...
  14. 电影版本名词解析(CAM,TS,TC,DVDSCR,DVDRIP,HR-HDTV)更新版
  15. 一点一滴解读网狐的加解密
  16. 温故知新之GPU计算
  17. 实现字符串首字母大写
  18. 【人工智能系列 - 智能硬件 - 01】演化硬件的概述
  19. 微信小程序 radio-group(排列) 及事件绑定、点击切换样式
  20. Java免费学习视频下载

热门文章

  1. 使用cubemx快速建立一个串口通信程序
  2. 货物配送问题的matlab,使用遗传算法求解物流中心配送问题
  3. Python的判断语句
  4. Dump微信PC端的界面Duilib文件
  5. Oracle数据库1521端口telnet不通问题
  6. macbook m1 无法启动mysql服务
  7. 硬件在环仿真(HiL)测试介绍
  8. Tomcat 动态资源服务器部署及应用
  9. 双击下载当日bing壁纸
  10. 招聘大师v6.7.2