变长模板参数
template<class T, class... Args>
void f(Args... args)
其中Args被称为模板参数包,可以通过class...或typename...来声明
args则代表变长参数集合,称作参数包
使用方式如下例所示:
#include <iostream>
using namespace std;
void doF1(int t1)
{
cout<<"arg1 type:"<<typeid(t1).name()<<" value:"<<t1<<endl;
}
void doF2(int t1, const char* t2)
{
cout<<"arg1 type:"<<typeid(t1).name()<<" value:"<<t1<<endl;
cout<<"arg2 type:"<<typeid(t2).name()<<" value:"<<t2<<endl;
}
template<class Fn, class... Args> //通过class...声明模板参数包Args
void f(Fn f, Args... args) //args为参数包
{
cout<<"arg number: "<<sizeof...(args)<<endl; //通过sizeof...可以获得参数包内参数的数量
f(args...); //可通过args...对参数包进行解包,传入最终函数doF
}
int main(){
f(doF1, 0);
f(doF2, 1, "hello");
return 0;
}
变长函数参数
较多使用的变长参数函数,最经典的便是printf
extern int printf(const char *format, ...);
以上是一个变长参数的函数声明。自己定义一个测试函数:
#include <stdarg.h>
#include <stdio.h>
int testparams(int count, ...)
{
va_list args;
va_start(args, count);
for (int i = 0; i < count; ++i)
{
int arg = va_arg(args, int);
printf("arg %d = %d", i, arg);
}
va_end(args);
return 0;
}
int main()
{
testparams(3, 10, 11, 12);
return 0;
}
变长参数函数的解析,使用到三个宏va_start
,va_arg
和va_end
,再看va_list
的定义 typedef char* va_list;
只是一个char指针。
这几个宏如何解析传入的参数呢?
函数的调用,是一个压栈,保存,跳转的过程。
简单的流程描述如下:
1、把参数从右到左依次压入栈;
2、调用call
指令,把下一条要执行的指令的地址作为返回地址入栈;(被调用函数执行完后会回到该地址继续执行)
3、当前的ebp(基址指针)入栈保存,然后把当前esp(栈顶指针)赋给ebp作为新函数栈帧的基址;
4、执行被调用函数,局部变量等入栈;
5、返回值放入eax,leave,ebp赋给esp,esp所存的地址赋给ebp;(这里可能需要拷贝临时返回对象)
从返回地址开始继续执行;(把返回地址所存的地址给eip)
由于开始的时候从右至左把参数压栈,va_start
传入最左侧的参数,往右的参数依次更早被压入栈,因此地址依次递增(栈顶地址最小)。va_arg
传入当前需要获得的参数的类型,便可以利用 sizeof
计算偏移量,依次获取后面的参数值。
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
#define _ADDRESSOF(v) (&const_cast<char&>(reinterpret_cast<const volatile char&>(v)))
#define __crt_va_start_a(ap, v) ((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)))
#define __crt_va_arg(ap, t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
#define __crt_va_end(ap) ((void)(ap = (va_list)0))
#define __crt_va_start(ap, x) ((void)(__vcrt_va_start_verify_argument_type<decltype(x)>(), __crt_va_start_a(ap, x)))
#define va_start __crt_va_start
#define va_arg __crt_va_arg
#define va_end __crt_va_end
上述宏定义中, _INTSIZEOF(n)
将地址的低2位指令,做内存的4字节对齐。每次取参数时,调用__crt_va_arg(ap,t)
,返回t类型参数地址的值,同时将ap偏移到t之后。最后,调用_crt_va_end(ap)
将ap置0.
变长参数的函数的使用及其原理看了宏定义是很好理解的。从上文可知,要使用变长参数函数的参数,我们必须知道传入的每个参数的类型。printf
中,有format
字符串中的特殊字符组合来解析后面的参数类型。但是当传入类的构造函数的参数时,我们并不知道每个参数都是什么类型,虽然参数能够依次传入函数,但无法解析并获取每个参数的数值。因此传统的变长参数函数并不足以解决传入任意构造函数参数的问题。
变长参数宏
可变参数宏有如下几种定义:
#define DEBUG(format, ...) printf (format, __VA_ARGS__)
#define DEBUG(format, args...) printf (format, args)
#define DEBUG(format, args...) printf (format, ##args)
#define DEBUG(format, ...) printf (format, ##__VA_ARGS__)
其中,缺省号代表一个可以变化的参数表。使用保留名 VA_ARGS 把参数传递给宏。当宏的调用展开时,那些符号序列集合将代替里面的**VA_ARGS**标识符。例如:
DEBUG("X = %d\n", X);
处理器会把宏的调用替换成:
printf("X = %d\n", X);
因为DEBUG()是一个可变参数宏,你能在每一次调用中传递不同数目的参数
案例:
#include<iostream>
#include<cstdarg>
using namespace std;
#define ADD(int_params,...) add(int_params,__VA_ARGS__,0)//_VA_ARGS__,这个宏可以取到变参
//求和函数
int add(int firstParam, ...)
{
va_list arg_ptr;
int sum = 0;
int nArgValue;
sum += firstParam;
va_start(arg_ptr, firstParam);
for (nArgValue = va_arg(arg_ptr, int); nArgValue != 0;)
{
sum = sum + nArgValue;
nArgValue = va_arg(arg_ptr, int);
}
va_end(arg_ptr);
return sum;
}
int main()
{
cout << "add = " << add(1, 2, 3, 0) << endl; //运行结果:6
cout << "ADD = " << ADD(1, 2, 3, 4, 5) << endl;
system("pause");
}
值得注意的是:##VA_ARGS 宏前面加上 ## 的作用在于,当可变参数的个数为0时,这里的 ## 起到把前面多余的","去掉的作用,否则会编译出错。
参考:
C++(11):变长模板_风静如云的博客-优快云博客_c++ 变长模板
C/C++:变长参数技巧汇总_CrackLewis的博客-优快云博客_c++ 变长参数