學習STL源碼遇到的C++新知識(三)可變參數

寫在前面

在array類源碼看到這樣一段代碼

template<class _First,
	class... _Rest>
	array(_First, _Rest...)
		-> array<typename _Enforce_same<_First, _Rest...>::type, 1 + sizeof...(_Rest)>;

於是決定深入瞭解一下c++變長參數的用法。

 

變長參數

可變參數是c++11的新特性,它允許函數的輸入參數爲不確定個,通常用“ ... ”代替。

void fun(int start, ...)

像上述代碼這樣,就聲明瞭一個可變參數的函數。它以start爲首,放入不確定個數的int類型的參數。

先來看一個代碼

#include <iostream>
#include<stdarg.h>
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;

 

可以看出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<decltype(x)>(), __crt_va_start_a(ap, x)))

__vcrt_assert_va_start_is_not_reference<decltype(x)>()的意思估計是如果start不是一個引用就斷言,實現如下:

extern "C++"
    {
        template <typename _Ty>
        struct __vcrt_va_list_is_reference
        {
            enum : bool { __the_value = false };
        };

        template <typename _Ty>
        struct __vcrt_va_list_is_reference<_Ty&>
        {
            enum : bool { __the_value = true };
        };

        template <typename _Ty>
        struct __vcrt_va_list_is_reference<_Ty&&>
        {
            enum : bool { __the_value = true };
        };

        template <typename _Ty>
        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<char&>(reinterpret_cast<const volatile char&>(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... Types> class className{};

用法如下:

#include <iostream>
using namespace std;

template<class... Types>
class Test {

};
int main()
{
	Test<int, float> p;//OK
	Test<int> p1;      //OK
	Test<> p2;         //OK
	Test<0> p3;        //ERROR
	return 0;          
}

函數形參包

我們將如下聲明定義爲“函數形參包”,它可以接受0個或多個函數形參的函數實參。

template<class... Types> void function(Types... args);

用法如下:

#include <iostream>
using namespace std;

template<class... Types>
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<class... Types>
fun(Types... args){

    args...;

}

 這個展開就是毫無意義的,因爲包的展開過程如下圖所示

可看出,解包過程是將包內數據逐個取出,經行操作,這個過程Types... args會被轉化爲Types arg, Types...  args,所以直接使用args...會報錯。

遞歸解包

我們知道解包過程以後,可以將代碼書寫如下:

#include <iostream>
using namespace std;

template<class Types>
void fun(Types arg) { //遞歸退出條件
	cout << arg << endl;
}
template<class T, class... Types>
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 <iostream>
using namespace std;

template<class Type>
void print(Type arg) {
	cout << arg << endl;
}

template<class... Types>
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 <iostream>
using namespace std;

template<class... Tail> //聲明
class Color {

};

template<class Header, class... Tail>
class Color<Header, Tail...> : public Color<Tail...> { //激發類的遞歸構造
public:
	Header h;
	Color() {
		cout << "length: " << sizeof...(Tail) << endl;
		cout << typeid(h).name() << endl;
	}
};

template<>
class Color<> { //遞歸結束標誌

};

int main()
{

	Color<int, float, double> color;
	return 0;
}

運行結果:

根據每次Tail的長度,來看這段代碼:class Color<Header, Tail...> : public Color<Tail...> 。我們可以推導出遞歸構造的過程:

class Color<int> : public Color<float, double>

class Color<int, float> : public Color<double>

class Color<in, float, doublet> : public Color<>

2.直接解包

template<class... Tail> //聲明
class Color {

};

template<class... Tail>
class Colors : public Color<Tail...> {

};

Colors<int, float, double> color;

上述代碼可以看出...是緊隨可變參數之後的,所以展開後等同於如下代碼:

template<class... Tail> //聲明
class Color {

};

class Colors : public Color<int, float, double> {

};

當然,還有下面這種寫法:

template<class... Tail> //聲明
class Color {

};

template<class... Tail>
class Colors : public Color<Tail>... {

};

Colors<int, float, double> color;

展開後等同於

template<class... Tail> //聲明
class Color {

};

template<class... Tail>
class Colors : public Color<int>, Color<float>, Color<double> {

};

到這裏可變參數就說完了,當然本人能力有限,推薦以下個人覺得關於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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章