学习STL源码遇到的C++新知识(三)可变参数

写在前面

在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这个头文件中):

  1. va_list

  2. void va_start( va_list arg_ptr, prev_param ); 

  3. type va_arg( va_list arg_ptr, type ); 

  4. void va_end( va_list arg_ptr ); 

va_list是声明了一个可变参数的列表,它使用va_start()进行初始化,并指定首个数值,va_arg()是访问下一个参数,类似于链表的next,最后使用va_end()释放内存等杂七杂八的东西;

变长参数的实现

va_list

首先打开va_list的源码,它的定义如下:

typedef char* va_list;

 学习STL源码遇到的C++新知识(三)可变参数_第1张图片

可以看出va_list是一个char型指针。

va_start()

打开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()的意思估计是如果start不是一个引用就断言,实现如下:

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()之后,程序将首参数的大小,对齐方式都保存了起来。

 

va_arg()

追踪找到源码,定义如下:

#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指针向下移动一位,使其指向下一个参数的地址。

 

va_end()

追踪源码,发现定义如下:

 #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...;但是参数包的展开并不是任何地方都可以进行的,它有如下展开方式:

  1. 表达式
  2. 初始化列表
  3. 基类描述列表
  4. 类成员初始化
  5. 模板参数列表
  6. 通用属性列表
  7. lambda函数的捕获列表

例如有如下伪代码:

template
fun(Types... args){

    args...;

}

 这个展开就是毫无意义的,因为包的展开过程如下图所示

学习STL源码遇到的C++新知识(三)可变参数_第2张图片

可看出,解包过程是将包内数据逐个取出,经行操作,这个过程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()函数并以此作为递归出口。

结果如下:

学习STL源码遇到的C++新知识(三)可变参数_第3张图片

 

非递归解包

 利用表达式解包,代码如下:

#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;          
}

我知道你一定会很惊讶,这是什么神仙写法?我们来分析一下。

 首先,我们知道...是解包符号,而...的位置决定了解包的方式,前面说过...的可使用位置,我们来回顾一下:

  1. 表达式
  2. 初始化列表
  3. 基类描述列表
  4. 类成员初始化
  5. 模板参数列表
  6. 通用属性列表
  7. lambda函数的捕获列表

我们写的这个 (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))... };

运行成功。 

 

可变参数模板类

继承解包

1.递归

#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;
}

运行结果:

学习STL源码遇到的C++新知识(三)可变参数_第4张图片

根据每次Tail的长度,来看这段代码:class Color : public Color 。我们可以推导出递归构造的过程:

class Color : public Color

class Color : public Color

class Color : public Color<>

2.直接解包

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

你可能感兴趣的:(C/C++)