C++中的變長參數深刻懂得。本站提示廣大學習愛好者:(C++中的變長參數深刻懂得)文章只能為提供參考,不一定能成為您想要的結果。以下是C++中的變長參數深刻懂得正文
媒介
在吸進的一個項目中為了應用同享內存和自界說內存池,我們本身界說了MemNew函數,且在函數外部關於非pod類型主動履行結構函數。在須要的處所挪用自界說的MemNew函數。如許就帶來一個成績,應用stl的類都有默許結構函數,和復制結構函數等。但應用同享內存和內存池的類能夠沒有默許結構函數,而是界說了多個參數的結構函數,因而若何將參數傳入MemNew函數便成了成績。
1、變長參數函數
起首回想一下較多應用的變長參數函數,最經典的就是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字符串中的特別字符組合來解析前面的參數類型。然則當傳入類的結構函數的參數時,我們其實不曉得每一個參數都是甚麼類型,固然參數可以或許順次傳入函數,但沒法解析並獲得每一個參數的數值。是以傳統的變長參數函數其實不足以處理傳入隨意率性結構函數參數的成績。
2、變長參數模板
我們須要用到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++能有所贊助,假如有疑問年夜家可以留言交換,感謝年夜家對的支撐。