C++中的變長參數(shù)深入理解
前言
在吸進的一個項目中為了使用共享內(nèi)存和自定義內(nèi)存池,我們自己定義了MemNew
函數(shù),且在函數(shù)內(nèi)部對于非pod類型自動執(zhí)行構(gòu)造函數(shù)。在需要的地方調(diào)用自定義的MemNew
函數(shù)。這樣就帶來一個問題,使用stl的類都有默認構(gòu)造函數(shù),以及復(fù)制構(gòu)造函數(shù)等。但使用共享內(nèi)存和內(nèi)存池的類可能沒有默認構(gòu)造函數(shù),而是定義了多個參數(shù)的構(gòu)造函數(shù),于是如何將參數(shù)傳入MemNew
函數(shù)便成了問題。
一、變長參數(shù)函數(shù)
首先回顧一下較多使用的變長參數(shù)函數(shù),最經(jīng)典的便是printf
。
extern int printf(const char *format, ...);
以上是一個變長參數(shù)的函數(shù)聲明。我們自己定義一個測試函數(shù):
#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; }
變長參數(shù)函數(shù)的解析,使用到三個宏va_start
,va_arg
和va_end
,再看va_list
的定義 typedef char* va_list;
只是一個char指針。
這幾個宏如何解析傳入的參數(shù)呢?
函數(shù)的調(diào)用,是一個壓棧,保存,跳轉(zhuǎn)的過程。
簡單的流程描述如下:
1、把參數(shù)從右到左依次壓入棧;
2、調(diào)用call
指令,把下一條要執(zhí)行的指令的地址作為返回地址入棧;(被調(diào)用函數(shù)執(zhí)行完后會回到該地址繼續(xù)執(zhí)行)
3、當(dāng)前的ebp(基址指針)入棧保存,然后把當(dāng)前esp(棧頂指針)賦給ebp作為新函數(shù)棧幀的基址;
4、執(zhí)行被調(diào)用函數(shù),局部變量等入棧;
5、返回值放入eax,leave,ebp賦給esp,esp所存的地址賦給ebp;(這里可能需要拷貝臨時返回對象)
從返回地址開始繼續(xù)執(zhí)行;(把返回地址所存的地址給eip)
由于開始的時候從右至左把參數(shù)壓棧,va_start
傳入最左側(cè)的參數(shù),往右的參數(shù)依次更早被壓入棧,因此地址依次遞增(棧頂?shù)刂纷钚。?code>va_arg傳入當(dāng)前需要獲得的參數(shù)的類型,便可以利用 sizeof
計算偏移量,依次獲取后面的參數(shù)值。
#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位指令,做內(nèi)存的4字節(jié)對齊。每次取參數(shù)時,調(diào)用__crt_va_arg(ap,t)
,返回t類型參數(shù)地址的值,同時將ap偏移到t之后。最后,調(diào)用_crt_va_end(ap)
將ap置0.
變長參數(shù)的函數(shù)的使用及其原理看了宏定義是很好理解的。從上文可知,要使用變長參數(shù)函數(shù)的參數(shù),我們必須知道傳入的每個參數(shù)的類型。printf
中,有format
字符串中的特殊字符組合來解析后面的參數(shù)類型。但是當(dāng)傳入類的構(gòu)造函數(shù)的參數(shù)時,我們并不知道每個參數(shù)都是什么類型,雖然參數(shù)能夠依次傳入函數(shù),但無法解析并獲取每個參數(shù)的數(shù)值。因此傳統(tǒng)的變長參數(shù)函數(shù)并不足以解決傳入任意構(gòu)造函數(shù)參數(shù)的問題。
二、變長參數(shù)模板
我們需要用到C++11的新特性,變長參數(shù)模板。
這里舉一個使用自定義內(nèi)存池的例子。定義一個內(nèi)存池類MemPool.h
,以count
個類型T為單元分配內(nèi)存,默認分配一個對象。每當(dāng)內(nèi)存內(nèi)空閑內(nèi)存不夠,則一次申請MEMPOOL_NEW_SIZE
個內(nèi)存對象。內(nèi)存池本身只負責(zé)內(nèi)存分配,不做初始化工作,因此不需要傳入任何參數(shù),只需實例化模板分配相應(yīng)類型的內(nèi)存即可。
#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
接下來在沒有變長參數(shù)的情況下,實現(xiàn)通用MemNew
和MemDelete
函數(shù)模板。這里不對函數(shù)模板作詳細解釋,用函數(shù)模板我們可以對不同的類型實現(xiàn)同樣的內(nèi)存池分配操作。如下:
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); } }
上述實現(xiàn)中,使用placement new
對申請的內(nèi)存進行構(gòu)造,使用了默認構(gòu)造函數(shù),當(dāng)申請內(nèi)存的類型不具備默認構(gòu)造函數(shù)時,placement new
將報錯。對于pod類型,可以省去調(diào)用構(gòu)造函數(shù)的過程。
引入C++11變長模板參數(shù)后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; }
以上函數(shù)定義包含了多個特性,后面我將一一解釋,其中class... Args
表示變長參數(shù)模板,函數(shù)參數(shù)中Args&& 為右值引用。std::forward<Args>
實現(xiàn)參數(shù)的完美轉(zhuǎn)發(fā)。這樣,無論傳入的類型具有什么樣的構(gòu)造函數(shù),都能夠完美執(zhí)行
C++11中引入了變長參數(shù)模板的概念,來解決參數(shù)個數(shù)不確定的模板。
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);
以上分別是使用變長參數(shù)類模板和變長參數(shù)函數(shù)模板的例子。
2.1變長參數(shù)函數(shù)模板
T... args
為形參包,其中args是模式,形參包中可以有0到任意多個參數(shù)。調(diào)用函數(shù)時,可以傳任意多個實參。對于函數(shù)定義來說,該如何使用參數(shù)包呢?在上文的MemNew
中,我們使用std::forward
依次將參數(shù)包傳入構(gòu)造函數(shù),并不關(guān)注每個參數(shù)具體是什么。如果需要,我們可以用sizeof...(args)
操作獲取參數(shù)個數(shù),也可以把參數(shù)包展開,對每個參數(shù)做更多的事。展開的方法有兩種,遞歸函數(shù),逗號表達式。
遞歸函數(shù)方式展開,模板推導(dǎo)的時候,一層層遞歸展開,最后到?jīng)]有參數(shù)時用定義的一般函數(shù)終止。
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
逗號表達式方式展開,利用數(shù)組的參數(shù)初始化列表和逗號表達式,逐一執(zhí)行print
每個參數(shù)。
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變長參數(shù)類模板
變長參數(shù)類模板,一般情況下可以方便我們做一些編譯期計算??梢酝ㄟ^偏特化和遞歸推導(dǎo)的方式依次展開模板參數(shù)。
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右值引用和完美轉(zhuǎn)發(fā)
對于變長參數(shù)函數(shù)模板,需要將形參包展開逐個處理的需求不多,更多的還是像本文的MemNew
這樣的需求,最終整個傳入某個現(xiàn)有的函數(shù)。我們把重點放在參數(shù)的傳遞上。
要理解右值引用,需要先說清楚左值和右值。左值是內(nèi)存中有確定存儲地址的對象的表達式的值;右值則是非左值的表達式的值。const
左值不可被賦值,臨時對象的右值可以被賦值。左值與右值的根本區(qū)別在于是否能用&運算符獲得內(nèi)存地址。
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 右值
理解左值和右值之后,再來看引用,對左值的引用就是左值引用,對右值(純右值和臨終值)的引用就是右值引用。
如下函數(shù)foo
,傳入int
類型,返回int
類型,這里傳入函數(shù)的參數(shù)0和返回值0都是右值(不能用&取得地址)。于是,未做優(yōu)化的情況下,傳入?yún)?shù)0的時候,我們需要把右值0拷貝給param
,函數(shù)返回的時候需要將0拷貝給臨時對象,臨時對象再拷貝給res。當(dāng)然現(xiàn)在的編譯器都做了返回值優(yōu)化,返回對象是直接創(chuàng)建在返回后的左值上的,這里只用來舉個例子
int foo(int param) { printf("%d", param); return 0; } int res = foo(0);
顯然,這里的拷貝都是多余的??赡芪覀儠胍獌?yōu)化,首先將參數(shù)int
改為int&
, 傳入左值引用,于是0無法傳入了,當(dāng)然我們可以改成const int&
,這樣終于省去了傳參的拷貝。
int foo(const int& param) { printf("%d", param); return 0; }
由于const int&
既可以是左值也可以是右值,傳入0或者int變量都能夠滿足。(但是似乎既然有左值引用的int&
類型,就應(yīng)該有對應(yīng)的傳入右值引用的類型int&&
)。另外,這里返回的右值0,似乎不通過拷貝就無法賦值給左值res
。
于是有了移動語義,把臨時對象的內(nèi)容直接移動給被賦值的左值對象(std::move
)。和右值引用,X&&是到數(shù)據(jù)類型X的右值引用。
int result = 0; int&& foo(int&& param) { printf("%d", param); return std::move(result); } int&& res = foo(0); int *pres = &res;
將foo
改為右值引用參數(shù)和返回值,返回右值引用,免去拷貝。這里res
是具名引用,運算符右側(cè)的右值引用作為左值,可以取地址。右值引用既有左值性質(zhì),也有右值性質(zhì)。
上述例子還只存在于拷貝的性能問題?;氐?code>MemNew這樣的函數(shù)模板。
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)); }
上述的前三種方式傳參,第一種首先有拷貝消耗,其次有的參數(shù)就是需要修改的左值。第二種方式則無法傳常數(shù)等右值。第三種方式雖然左值右值都能傳,卻無法對傳入的參數(shù)進行修改。第四種方式使用右值引用,可以解決參數(shù)完美轉(zhuǎn)發(fā)的問題。
std::forward能夠根據(jù)實參的數(shù)據(jù)類型,返回相應(yīng)類型的左值和右值引用,將參數(shù)完整不動的傳遞下去。
解釋這個原理涉及到引用塌縮規(guī)則
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); }
對于函數(shù)模板
template<class T> T* Test(T&& arg) { return new T(std::forward<T>(arg)); }
當(dāng)傳入實參為X類型左值時,T為X&,最后的類型為X&。當(dāng)實參為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)); }
可以看到最終實參總是被推導(dǎo)為和傳入時相同的類型引用。
至此,我們討論了變長參數(shù)模板,討論了右值引用和函數(shù)模板的完美轉(zhuǎn)發(fā),完整的解釋了MemNew
對任意多個參數(shù)的構(gòu)造函數(shù)的參數(shù)傳遞過程。利用變長參數(shù)函數(shù)模板,右值引用和std::forward
,可以完成參數(shù)的完美轉(zhuǎn)發(fā)。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家學(xué)習(xí)或者使用C++能有所幫助,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
- Lua學(xué)習(xí)筆記之函數(shù)、變長參數(shù)、closure(閉包)、select等
- oracle中變長數(shù)組varray,嵌套表,集合使用方法
- 淺談C++內(nèi)存分配及變長數(shù)組的動態(tài)分配
- C語言可變參數(shù)函數(shù)詳解示例
- C/C++宏定義的可變參數(shù)詳細解析
- C++可變參數(shù)的實現(xiàn)方法
- 關(guān)于C/C++中可變參數(shù)的詳細介紹(va_list,va_start,va_arg,va_end)
- C/C++中可變參數(shù)的用法詳細解析
- C++用指針變量作為函數(shù)的參數(shù)接受數(shù)組的值的問題詳細總結(jié)
- C++可變參數(shù)的函數(shù)與模板實例分析
相關(guān)文章
C++ Opencv imfill孔洞填充函數(shù)的實現(xiàn)思路與代碼
在Matlab下,使用imfill可以很容易的完成孔洞填充操作,下面這篇文章主要給大家介紹了關(guān)于C++ Opencv imfill孔洞填充函數(shù)的實現(xiàn)思路與代碼,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下2021-09-09C++友元函數(shù)與拷貝構(gòu)造函數(shù)詳解
這篇文章主要介紹了C++友元函數(shù)與拷貝構(gòu)造函數(shù),需要的朋友可以參考下2014-07-07