写在前面
在array类源码看到这样一段代码
template
array(_First, _Rest...)
-> array::type, 1 + sizeof...(_Rest)>;
于是决定深入了解一下c++变长参数的用法。
变长参数
可变参数是c++11的新特性,它允许函数的输入参数为不确定个,通常用“ ... ”代替。
void fun(int start, ...)
像上述代码这样,就声明了一个可变参数的函数。它以start为首,放入不确定个数的int类型的参数。
先来看一个代码
#include
#include
using namespace std;
void fun(int start, ...) {
va_list args;
va_start(args, start);
int arg = start;
while (arg != -1) {
cout << arg << " ";
arg = va_arg(args, int);
}
va_end(args);
}
int main()
{
fun(1, 2, 3, 4, 5, 6, 7, 8, 9, -1);
return 0;
}
解释:
在main函数中的fun函数的-1参数是截止标志(可变参数不知道会有几个参数被传入,所以要手动设置结束方式,一般第一个参数为参数个数,或者最后一个参数为结尾标志,本次使用后者)
观察fun函数我们发现,访问可变参数使用了以下变量和方法(这些都在stdarg.h这个头文件中):
va_list
void va_start( va_list arg_ptr, prev_param );
type va_arg( va_list arg_ptr, type );
void va_end( va_list arg_ptr );
va_list是声明了一个可变参数的列表,它使用va_start()进行初始化,并指定首个数值,va_arg()是访问下一个参数,类似于链表的next,最后使用va_end()释放内存等杂七杂八的东西;
变长参数的实现
首先打开va_list的源码,它的定义如下:
typedef char* va_list;
可以看出va_list是一个char型指针。
打开va_start(),我们发现他是重名了 __crt_va_start
#define va_start __crt_va_start
继续追踪,发现以下代码
__crt_va_start(ap, x)
((void)(__vcrt_assert_va_start_is_not_reference(), __crt_va_start_a(ap, x)))
__vcrt_assert_va_start_is_not_reference
extern "C++"
{
template
struct __vcrt_va_list_is_reference
{
enum : bool { __the_value = false };
};
template
struct __vcrt_va_list_is_reference<_Ty&>
{
enum : bool { __the_value = true };
};
template
struct __vcrt_va_list_is_reference<_Ty&&>
{
enum : bool { __the_value = true };
};
template
struct __vcrt_assert_va_start_is_not_reference
{
static_assert(!__vcrt_va_list_is_reference<_Ty>::__the_value,
"va_start argument must not have reference type and must not be parenthesized");
};
} // extern "C++"
继续追踪__crt_va_start_a(ap, x),发现下面代码
#define __crt_va_start_a(ap, x) ((void)(__va_start(&ap, x)))
追踪__va_start(&ap, x),发现代码会根据不同的操作系统进行__va_start(&ap, x)的定义,但是实现基本都一样,我们选取_M_HYBRID_X86_ARM64这个操作系统下的代码,如下:
void __cdecl __va_start(va_list*, ...);
#define __crt_va_start_a(ap,v)
((void)(__va_start(&ap, _ADDRESSOF(v), _SLOTSIZEOF(v), __alignof(v), _ADDRESSOF(v))))
好了,我们关注一下第一行代码里面的__cdecl,百度百科给了以下定义:
__cdecl 是C Declaration的缩写(declaration,声明),表示C语言默认的函数调用方法:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。被调用函数不会要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。(来自:https://baike.baidu.com/item/__cdecl)
我们现在知道了va_start会把所有参数从右到左依次入栈。
接下来看第三行代码,我们找到_ADDRESSOF(v)的定义,如下:
#define _ADDRESSOF(v) (&const_cast(reinterpret_cast(v)))
可以发现这是把v转换为了char类型的引用。
_SLOTSIZEOF(v)就是sizeof()函数。
__alignof(v),C++11 引入 alignof 运算符,该运算符返回指定类型的对齐方式(以字节为单位)。
可以看出,在调用了va_start()之后,程序将首参数的大小,对齐方式都保存了起来。
追踪找到源码,定义如下:
#define __crt_va_arg(ap, t) \
((sizeof(t) > sizeof(__int64) || (sizeof(t) & (sizeof(t) - 1)) != 0) \
? **(t**)((ap += sizeof(__int64)) - sizeof(__int64)) \
: *(t* )((ap += sizeof(__int64)) - sizeof(__int64)))
发现还可以继续追踪,源码如下:
#define __crt_va_arg(ap, t)
(*(t*)((ap += _SLOTSIZEOF(t) + _APALIGN(t,ap)) - _SLOTSIZEOF(t)))
可以看出功能就是将ap指针向下移动一位,使其指向下一个参数的地址。
追踪源码,发现定义如下:
#define __crt_va_end(ap) ((void)(ap = (va_list)0))
具体作用是释放了指针。
可变参数的实现以及访问主要使用了va_list类型配合va_start()、va_arg()、va_end()函数使用。va_list指向整个参数列表,va_start()初始化va_list,va_arg()使得va_list指向下一参数,va_end()释放指针。
变长参数模板
我们将如下声明定义为“模板形参包”,它可以接受0个或多个模板形参的模板实参。
template class className{};
用法如下:
#include
using namespace std;
template
class Test {
};
int main()
{
Test p;//OK
Test p1; //OK
Test<> p2; //OK
Test<0> p3; //ERROR
return 0;
}
我们将如下声明定义为“函数形参包”,它可以接受0个或多个函数形参的函数实参。
template void function(Types... args);
用法如下:
#include
using namespace std;
template
void fun(Types... args) {
}
int main()
{
fun();
fun(1);
fun(2, 2.5);
fun(1, 2.5, "Test");
return 0;
}
形参包只有以上两种用法,要么是一个模板形参包,要么是一个函数形参包。
函数形参包的解包
那么在拿到函数形参包之后该怎么获取到里面的内容呢?我们把打开参数包的行为叫做解包。
在Types... args中Types... args 为形参包,其中args是模式,解包一般使用...,例如args...;但是参数包的展开并不是任何地方都可以进行的,它有如下展开方式:
例如有如下伪代码:
template
fun(Types... args){
args...;
}
这个展开就是毫无意义的,因为包的展开过程如下图所示
可看出,解包过程是将包内数据逐个取出,经行操作,这个过程Types... args会被转化为Types arg, Types... args,所以直接使用args...会报错。
我们知道解包过程以后,可以将代码书写如下:
#include
using namespace std;
template
void fun(Types arg) { //递归退出条件
cout << arg << endl;
}
template
void fun(T arg, Types... args) {
cout << arg << endl;
fun(args...);
}
int main()
{
fun(1, 2.5, "Test");
return 0;
}
由于每次解包Types.. args会被分成当前取出对象和剩下包,所以将函数参数写为T arg, Types... args,这样就可以接收解包后的数据, 在解包最后一步的时候只有单个参数arg,所以重载fun()函数并以此作为递归出口。
结果如下:
利用表达式解包,代码如下:
#include
using namespace std;
template
void print(Type arg) {
cout << arg << endl;
}
template
void fun(Types... args) {
int test[] = {
(print(args), 0)...
};
}
int main()
{
fun(1, 2.5, "Test");
return 0;
}
我知道你一定会很惊讶,这是什么神仙写法?我们来分析一下。
首先,我们知道...是解包符号,而...的位置决定了解包的方式,前面说过...的可使用位置,我们来回顾一下:
我们写的这个 (print(args), 0)... 不就是第一种吗,首先“,”是个运算符,他只保留最后一个表达式,但是前面的也会运行,再来看...的位置,它写在了表达式的后面,也就是说它会带着表达式一起展开,展开后如下:
int test[] = {
(print(arg1), 0), (print(arg2), 0), (print(arg3), 0)
};
这样的活args传来的参数都被打印了出来,并且还初始化了一个长度为sizeof...(args)(sizeof...()可以获得传入参数的个数)的内容全为0的数组;
回顾一下,以Types.. args为例,解包时...紧随可变参数之后,如args..., 解包得到的是arg1, arg2, arg3(注意这里面的逗号,不是为了分割,而实这个逗号也是存在的);如果...放在含有args的表达式后面,那么将带着表达式一起展开,如(print(args), 0)...展开后为(print(arg1), 0), (print(arg2), 0), (print(arg3), 0)(假设只有3个参数)。
但是如果你想按如下方式展开,是会报错的
int a = 0;
(a += (print(args), 0))...;
毕竟,这样...会与;冲突,报错如下:
修改一下呗
int a = 0;
int s[] = { (a += (print(args), 0))... };
运行成功。
#include
using namespace std;
template //声明
class Color {
};
template
class Color : public Color { //激发类的递归构造
public:
Header h;
Color() {
cout << "length: " << sizeof...(Tail) << endl;
cout << typeid(h).name() << endl;
}
};
template<>
class Color<> { //递归结束标志
};
int main()
{
Color color;
return 0;
}
运行结果:
根据每次Tail的长度,来看这段代码:class Color
class Color
class Color
class Color
template //声明
class Color {
};
template
class Colors : public Color {
};
Colors color;
上述代码可以看出...是紧随可变参数之后的,所以展开后等同于如下代码:
template //声明
class Color {
};
class Colors : public Color {
};
当然,还有下面这种写法:
template //声明
class Color {
};
template
class Colors : public Color... {
};
Colors color;
展开后等同于
template //声明
class Color {
};
template
class Colors : public Color, Color, Color {
};
到这里可变参数就说完了,当然本人能力有限,推荐以下个人觉得关于c++变长参数写的比较好的博客,本博客也对其有所参考:
https://www.cnblogs.com/qicosmos/p/4325949.html
https://blog.csdn.net/tennysonsky/article/details/77389891
https://www.cnblogs.com/kerngeeksund/p/11175769.html
https://www.cnblogs.com/kevonyang/p/5932059.html