寫在前面
在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這個頭文件中):
-
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
首先打開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...;但是參數包的展開並不是任何地方都可以進行的,它有如下展開方式:
- 表達式
- 初始化列表
- 基類描述列表
- 類成員初始化
- 模板參數列表
- 通用屬性列表
- 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;
}
我知道你一定會很驚訝,這是什麼神仙寫法?我們來分析一下。
首先,我們知道...是解包符號,而...的位置決定了解包的方式,前面說過...的可使用位置,我們來回顧一下:
- 表達式
- 初始化列表
- 基類描述列表
- 類成員初始化
- 模板參數列表
- 通用屬性列表
- 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