C/C++:变长参数技巧汇总
C/C++常见的变长参数技巧包括变长模板、变长函数参数和变长宏参数。
变参数函数
最常见的变参数函数就是printf
、scanf
之类的,利用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(<); \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
,则args
、std::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++:变长参数技巧汇总相关推荐
- 变长参数模板 和 外部模板
变长参数模板 解释 C++03只有固定模板参数.C++11 加入新的表示法,允许任意个数.任意类别的模板参数,不必在定义时将参数的个数固定. 变长模板.变长参数是依靠C++11新引入的参数包的机制实现 ...
- 关于c++变长参数列表总结
2019独角兽企业重金招聘Python工程师标准>>> 写在前面 在C++语言中,有两个三个(???)地方用到了"..."这个符号,分别是: 变长参数列表.下面用 ...
- Java变长参数应该注意的问题
从Java SDK 5开始,Java就支持了变长参数,但是在使用时应该注意如下问题: 1.变长参数只能放在最后. 2.如果出现重载情况,应该注意模糊性. 例如: class VarArgs { sta ...
- c语言 宏 变长参数,科学网—C/C++中处理变长参数函数(Variadic Function)的几个宏 - 彭彬的博文...
近日在模式中进行非线性方程组求解时遇到变长参数函数的问题,以前从来没有自己写过变长参数的函数,于是补了一下课,将近日对该小问题的学习和理解整理如下. 一.变长参数函数(variadic functio ...
- c/c++十七: 变长参数
#include"c1.h" #include<stdarg.h> typedef int ElemType;ElemType Max(int num,...){ // ...
- *args and **kwargs in Python 变长参数
原文链接 变长参数 args(非关键字参数) def myFun(*argv): for arg in argv: print (arg)myFun('Hello', 'Welcome', 'to', ...
- Spark UDF变长参数的二三事儿
在复杂业务逻辑中,我们经常会用到Spark的UDF,当一个UDF需要传入多列的内容并进行处理时,UDF的传参该怎么做呢? 下面通过变长参数引出,逐一介绍三种可行方法以及一些不可行的尝试... 引子 变 ...
- java 变长参数 知乎_变长参数探究
前言 变长参数,指的是函数参数数量可变,或者说函数接受参数的数量可以不固定.实际上,我们最开始学C语言的时候,就用到了这样的函数:printf,它接受任意数量的参数,向终端格式化输出字符串.本文就来探 ...
- matlab 变长参数,变长参数函数的概念
分享一个2015年华为笔试知识点:变长参数函数 变长参数的函数即参数个数可变.参数类型不定 的函数. 设计一个参数个数可变.参数类型不定的函数是可能的,最常见的例子是printf函数.scanf函数和 ...
最新文章
- mysql隐式转换造成索引失效的事故总结
- pc控制iphone的软件_如何在iPhone上下载升级最新的iOS 13公测版
- boost::type_index模块constexpr相关的测试程序
- python redis模块connectionerror_PHP程序连接Redis报read error on connection问题
- iOS 编写高质量Objective-C代码(六)
- python爬取百度域名注册_python爬取百度域名_python爬取百度搜索結果url匯總
- 用CSS使DIV水平居中
- appium+python 【Mac】Android夜神模拟器
- 【洛谷P1256】公路修建(问题分析+最小生成树prim法)
- 机器学习和模式识别怎么区分?
- 如何防止局域网病毒春风吹又生--之一
- 一篇爽文带你全面了解mysql的索引
- 计算机在康复治疗学中的应用,等速运动在康复医学中的应用 【康复医学讨论版】...
- 电影版本名词解析(CAM,TS,TC,DVDSCR,DVDRIP,HR-HDTV)更新版
- 一点一滴解读网狐的加解密
- 温故知新之GPU计算
- 实现字符串首字母大写
- 【人工智能系列 - 智能硬件 - 01】演化硬件的概述
- 微信小程序 radio-group(排列) 及事件绑定、点击切换样式
- Java免费学习视频下载