C++中的變長參數深化了解。本站提示廣大學習愛好者:(C++中的變長參數深化了解)文章只能為提供參考,不一定能成為您想要的結果。以下是C++中的變長參數深化了解正文
前言
在吸進的一個項目中為了運用共享內存和自定義內存池,我們自己定義了MemNew
函數,且在函數外部關於非pod類型自動執行結構函數。在需求的中央調用自定義的MemNew
函數。這樣就帶來一個問題,運用stl的類都有默許結構函數,以及復制結構函數等。但運用共享內存和內存池的類能夠沒有默許結構函數,而是定義了多個參數的結構函數,於是如何將參數傳入MemNew
函數便成了問題。
一、變長參數函數
首先回憶一下較多運用的變長參數函數,最經典的便是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
字符串中的特殊字符組合來解析前面的參數類型。但是當傳入類的結構函數的參數時,我們並不知道每個參數都是什麼類型,雖然參數可以順次傳入函數,但無法解析並獲取每個參數的數值。因而傳統的變長參數函數並缺乏以處理傳入恣意結構函數參數的問題。
二、變長參數模板
我們需求用到C++11的新特性,變長參數模板。
這裡舉一個運用自定義內存池的例子。定義一個內存池類MemPool.h
,以count
個類型T為單元分配內存,默許分配一個對象。每當內存內閒暇內存不夠,則一次請求MEMPOOL_NEW_SIZE
個內存對象。內存池自身只擔任內存分配,不做初始化任務,因而不需求傳入任何參數,只需實例化模板分配相應類型的內存即可。
#ifndef UTIL_MEMPOOL_H #define UTIL_MEMPOOL_H #include <stdlib.h> #define MEMPOOL_NEW_SIZE 8 template<typename T, size_t count = 1> class MemPool { private: union MemObj { char _obj[1]; MemObj* _freelink; }; public: static void* Allocate() { if (!_freelist) { refill(); } MemObj* alloc_mem = _freelist; _freelist = _freelist->_freelink; ++_size; return (void*)alloc_mem; } static void DeAllocate(void* p) { MemObj* q = (MemObj*)p; q->_freelink = _freelist; _freelist = q; --_size; } static size_t TotalSize() { return _totalsize; } static size_t Size() { return _size; } private: static void refill() { size_t size = sizeof(T) * count; char* new_mem = (char*)malloc(size * MEMPOOL_NEW_SIZE); for (int i = 0; i < MEMPOOL_NEW_SIZE; ++i) { MemObj* free_mem = (MemObj*)(new_mem + i * size); free_mem->_freelink = _freelist; _freelist = free_mem; } _totalsize += MEMPOOL_NEW_SIZE; } static MemObj* _freelist; static size_t _totalsize; static size_t _size; }; template<typename T, size_t count> typename MemPool<T, count>::MemObj* MemPool<T, count>::_freelist = NULL; template<typename T, size_t count> size_t MemPool<T, count>::_totalsize = 0; template<typename T, size_t count> size_t MemPool<T, count>::_size = 0; #endif
接上去在沒有變長參數的狀況下,完成通用MemNew
和MemDelete
函數模板。這裡不對函數模板作詳細解釋,用函數模板我們可以對不同的類型完成異樣的內存池分配操作。如下:
template<class T> T *MemNew(size_t count) { T *p = (T*)MemPool<T, count>::Allocate(); if (p != NULL) { if (!std::is_pod<T>::value) { for (size_t i = 0; i < count; ++i) { new (&p[i]) T(); } } } return p; } template<class T> T *MemDelete(T *p, size_t count) { if (p != NULL) { if (!std::is_pod<T>::value) { for (size_t i = 0; i < count; ++i) { p[i].~T(); } } MemPool<T, count>::DeAllocate(p); } }
上述完成中,運用placement new
對請求的內存停止結構,運用了默許結構函數,當請求內存的類型不具有默許結構函數時,placement new
將報錯。關於pod類型,可以省去調用結構函數的進程。
引入C++11變長模板參數後MemNew修正為如下
template<class T, class... Args> T *MemNew(size_t count, Args&&... args) { T *p = (T*)MemPool<T, count>::Allocate(); if (p != NULL) { if (!std::is_pod<T>::value) { for (size_t i = 0; i < count; ++i) { new (&p[i]) T(std::forward<Args>(args)...); } } } return p; }
以上函數定義包括了多個特性,前面我將逐個解釋,其中class... Args
表示變長參數模板,函數參數中Args&& 為右值援用。std::forward<Args>
完成參數的完滿轉發。這樣,無論傳入的類型具有什麼樣的結構函數,都可以完滿執行
C++11中引入了變長參數模板的概念,來處理參數個數不確定的模板。
template<class... T> class Test {}; Test<> test0; Test<int> test1; Test<int,int> test2; Test<int,int,long> test3; template<class... T> void test(T... args); test(); test<int>(0); test<int,int,long>(0,0,0L);
以上辨別是運用變長參數類模板和變長參數函數模板的例子。
2.1變長參數函數模板
T... args
為形參包,其中args是形式,形參包中可以有0就任意多個參數。調用函數時,可以傳恣意多個實參。關於函數定義來說,該如何運用參數包呢?在上文的MemNew
中,我們運用std::forward
順次將參數包傳入結構函數,並不關注每個參數詳細是什麼。假如需求,我們可以用sizeof...(args)
操作獲取參數個數,也可以把參數包展開,對每個參數做更多的事。展開的辦法有兩種,遞歸函數,逗號表達式。
遞歸函數方式展開,模板推導的時分,一層層遞歸展開,最後到沒有參數時用定義的普通函數終止。
void test() { } template<class T, class... Args> void test(T first, Args... args) { std::cout << typeid(T).name() << " " << first << std::endl; test(args...); } test<int, int, long>(0, 0, 0L); output: int 0 int 0 long 0
逗號表達式方式展開,應用數組的參數初始化列表和逗號表達式,逐一執行print
每個參數。
template<class T> void print(T arg) { std::cout << typeid(T).name() << " " << arg << std::endl; } template<class... Args> void test(Args... args) { int arr[] = { (print(args), 0)... }; } test(0, 0, 0L); output: int 0 int 0 long 0
2.2變長參數類模板
變長參數類模板,普通狀況下可以方便我們做一些編譯期計算。可以經過偏特化和遞歸推導的方式順次展開模板參數。
template<class T, class... Types> class Test { public: enum { value = Test<T>::value + Test<Types...>::value, }; }; template<class T> class Test<T> { public: enum { value = sizeof(T), }; }; Test<int, int, long> test; std::cout << test.value; output: 12
2.3右值援用和完滿轉發
關於變長參數函數模板,需求將形參包展開逐一處置的需求不多,更多的還是像本文的MemNew
這樣的需求,最終整個傳入某個現有的函數。我們把重點放在參數的傳遞上。
要了解右值援用,需求先說清楚左值和右值。左值是內存中有確定存儲地址的對象的表達式的值;右值則是非左值的表達式的值。const
左值不可被賦值,暫時對象的右值可以被賦值。左值與右值的基本區別在於能否能用&運算符取得內存地址。
int i =0;//i 左值 int *p = &i;// i 左值 int& foo(); foo() = 42;// foo() 左值 int* p1 = &foo();// foo() 左值 int foo1(); int j = 0; j = foo1();// foo 右值 int k = j + 1;// j + 1 右值 int *p2 = &foo1(); // 錯誤,無法取右值的地址 j = 1;// 1 右值
了解左值和右值之後,再來看援用,對左值的援用就是左值援用,對右值(純右值和臨終值)的援用就是右值援用。
如下函數foo
,傳入int
類型,前往int
類型,這裡傳入函數的參數0和前往值0都是右值(不能用&獲得地址)。於是,未做優化的狀況下,傳入參數0的時分,我們需求把右值0拷貝給param
,函數前往的時分需求將0拷貝給暫時對象,暫時對象再拷貝給res。當然如今的編譯器都做了前往值優化,前往對象是直接創立在前往後的左值上的,這裡只用來舉個例子
int foo(int param) { printf("%d", param); return 0; } int res = foo(0);
顯然,這裡的拷貝都是多余的。能夠我們會想要優化,首先將參數int
改為int&
, 傳入左值援用,於是0無法傳入了,當然我們可以改成const int&
,這樣終於省去了傳參的拷貝。
int foo(const int& param) { printf("%d", param); return 0; }
由於const int&
既可以是左值也可以是右值,傳入0或許int變量都可以滿足。(但是似乎既然有左值援用的int&
類型,就應該有對應的傳入右值援用的類型int&&
)。另外,這裡前往的右值0,似乎不經過拷貝就無法賦值給左值res
。
於是有了挪動語義,把暫時對象的內容直接挪動給被賦值的左值對象(std::move
)。和右值援用,X&&是到數據類型X的右值援用。
int result = 0; int&& foo(int&& param) { printf("%d", param); return std::move(result); } int&& res = foo(0); int *pres = &res;
將foo
改為右值援用參數和前往值,前往右值援用,免去拷貝。這裡res
是具名援用,運算符右側的右值援用作為左值,可以取地址。右值援用既有左值性質,也有右值性質。
上述例子還只存在於拷貝的功能問題。回到MemNew
這樣的函數模板。
template<class T> T* Test(T arg) { return new T(arg); } template<class T> T* Test(T& arg) { return new T(arg); } template<class T> T* Test(const T& arg) { return new T(arg); } template<class T> T* Test(T&& arg) { return new T(std::forward<T>(arg)); }
上述的前三種方式傳參,第一種首先有拷貝耗費,其次有的參數就是需求修正的左值。第二種方式則無法傳常數等右值。第三種方式雖然左值右值都能傳,卻無法對傳入的參數停止修正。第四種方式運用右值援用,可以處理參數完滿轉發的問題。
std::forward可以依據實參的數據類型,前往相應類型的左值和右值援用,將參數完好不動的傳遞下去。
解釋這個原理觸及到援用塌縮規則
T& & ->T&
T& &&->T&
T&& &->T&
T&& &&->T&&
template< class T > struct remove_reference {typedef T type;}; template< class T > struct remove_reference<T&> {typedef T type;}; template< class T > struct remove_reference<T&&> {typedef T type;}; template< class T > T&& forward( typename std::remove_reference<T>::type& t ) { return static_cast<T&&>(t); } template<class T> typename std::remove_reference<T>::type&& move(T&& a) noexcept { return static_cast<typename std::remove_reference<T>::type&&>(a); }
關於函數模板
template<class T> T* Test(T&& arg) { return new T(std::forward<T>(arg)); }
當傳入實參為X類型左值時,T為X&,最後的類型為X&。當實參為X類型右值時,T為X,最後的類型為X&&。
x為左值時:
X x; Test(x);
T為X&,實例化後
X& && std::forward(remove_reference<X&>::type& a) noexcept { return static_cast<X& &&>(a); } X* Test(X& && arg) { return new X(std::forward<X&>(arg)); } // 塌陷後 X& std::forward(X& a) { return static_cast<X&>(a); } X* Test(X& arg) { return new X(std::forward<X&>(arg)); }
x為右值時:
X foo(); Test(foo());
T為X,實例化後
X&& std::forward(remove_reference<X>::type& a) noexcept { return static_cast<X&&>(a); } X* Test(X&& arg) { return new X(std::forward<X>(arg)); } // 塌陷後 X&& std::forward(X& a) { return static_cast<X&&>(a); } X* Test(X&& arg) { return new X(std::forward<X>(arg)); }
可以看到最終實參總是被推導為和傳入時相反的類型援用。
至此,我們討論了變長參數模板,討論了右值援用和函數模板的完滿轉發,完好的解釋了MemNew
對恣意多個參數的結構函數的參數傳遞進程。應用變長參數函數模板,右值援用和std::forward
,可以完成參數的完滿轉發。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家學習或許運用C++能有所協助,假如有疑問大家可以留言交流,謝謝大家對的支持。