新参与的项目中,为了使用共享内存和自定义内存池,我们自己定义了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指针。
这几个宏如何解析传入的参数呢?
函数的调用,是一个压栈,保存,跳转的过程。简单的流程描述如下:
- 把参数从右到左依次压入栈;
- 调用call指令,把下一条要执行的指令的地址作为返回地址入栈;(被调用函数执行完后会回到该地址继续执行)
- 当前的ebp(基址指针)入栈保存,然后把当前esp(栈顶指针)赋给ebp作为新函数栈帧的基址;
- 执行被调用函数,局部变量等入栈;
- 返回值放入eax,leave,ebp赋给esp,esp所存的地址赋给ebp;(这里可能需要拷贝临时返回对象)
- 从返回地址开始继续执行;(把返回地址所存的地址给eip)
由于开始的时候从右至左把参数压栈,va_start 传入最左侧的参数,往右的参数依次更早被压入栈,因此地址依次递增(栈顶地址最小)。va_arg传入当前需要获得的参数的类型,便可以利用 sizeof 计算偏移量,依次获取后面的参数值。
1 #define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
2
3 #define _ADDRESSOF(v) (&const_cast<char&>(reinterpret_cast<const volatile char&>(v)))
4
5 #define __crt_va_start_a(ap, v) ((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)))
6 #define __crt_va_arg(ap, t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
7 #define __crt_va_end(ap) ((void)(ap = (va_list)0))
8
9 #define __crt_va_start(ap, x) ((void)(__vcrt_va_start_verify_argument_type<decltype(x)>(), __crt_va_start_a(ap, x)))
10
11 #define va_start __crt_va_start
12 #define va_arg __crt_va_arg
13 #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个内存对象。内存池本身只负责内存分配,不做初始化工作,因此不需要传入任何参数,只需实例化模板分配相应类型的内存即可。
1 #ifndef UTIL_MEMPOOL_H
2 #define UTIL_MEMPOOL_H
3
4 #include <stdlib.h>
5
6 #define MEMPOOL_NEW_SIZE 8
7
8 template<typename T, size_t count = 1>
9 class MemPool
10 {
11 private:
12 union MemObj {
13 char _obj[1];
14 MemObj* _freelink;
15 };
16
17 public:
18 static void* Allocate()
19 {
20 if (!_freelist) {
21 refill();
22 }
23 MemObj* alloc_mem = _freelist;
24 _freelist = _freelist->_freelink;
25 ++_size;
26 return (void*)alloc_mem;
27 }
28
29 static void DeAllocate(void* p)
30 {
31 MemObj* q = (MemObj*)p;
32 q->_freelink = _freelist;
33 _freelist = q;
34 --_size;
35 }
36
37 static size_t TotalSize() {
38 return _totalsize;
39 }
40
41 static size_t Size() {
42 return _size;
43 }
44 private:
45 static void refill()
46 {
47 size_t size = sizeof(T) * count;
48 char* new_mem = (char*)malloc(size * MEMPOOL_NEW_SIZE);
49 for (int i = 0; i < MEMPOOL_NEW_SIZE; ++i) {
50 MemObj* free_mem = (MemObj*)(new_mem + i * size);
51 free_mem->_freelink = _freelist;
52 _freelist = free_mem;
53 }
54 _totalsize += MEMPOOL_NEW_SIZE;
55 }
56
57 static MemObj* _freelist;
58 static size_t _totalsize;
59 static size_t _size;
60 };
61
62 template<typename T, size_t count>
63 typename MemPool<T, count>::MemObj* MemPool<T, count>::_freelist = NULL;
64
65 template<typename T, size_t count>
66 size_t MemPool<T, count>::_totalsize = 0;
67
68 template<typename T, size_t count>
69 size_t MemPool<T, count>::_size = 0;
70 #endif
接下来在没有变长参数的情况下,实现通用MemNew和MemDelete函数模板。这里不对函数模板作详细解释,用函数模板我们可以对不同的类型实现同样的内存池分配操作。如下:
1 template<class T>
2 T *MemNew(size_t count)
3 {
4 T *p = (T*)MemPool<T, count>::Allocate();
5 if (p != NULL)
6 {
7 if (!std::is_pod<T>::value)
8 {
9 for (size_t i = 0; i < count; ++i)
10 {
11 new (&p[i]) T();
12 }
13 }
14 }
15 return p;
16 }
17
18 template<class T>
19 T *MemDelete(T *p, size_t count)
20 {
21 if (p != NULL)
22 {
23 if (!std::is_pod<T>::value)
24 {
25 for (size_t i = 0; i < count; ++i)
26 {
27 p[i].~T();
28 }
29 }
30 MemPool<T, count>::DeAllocate(p);
31 }
32 }
上述实现中,使用placement new对申请的内存进行构造,使用了默认构造函数,当申请内存的类型不具备默认构造函数时,placement new将报错。对于pod类型,可以省去调用构造函数的过程。
引入C++11变长模板参数后MemNew修改为如下
1 template<class T, class... Args>
2 T *MemNew(size_t count, Args&&... args)
3 {
4 T *p = (T*)MemPool<T, count>::Allocate();
5 if (p != NULL)
6 {
7 if (!std::is_pod<T>::value)
8 {
9 for (size_t i = 0; i < count; ++i)
10 {
11 new (&p[i]) T(std::forward<Args>(args)...);
12 }
13 }
14 }
15 return p;
16 }
以上函数定义包含了多个特性,后面我将一一解释,其中class... Args 表示变长参数模板,函数参数中Args&& 为右值引用。std::forward<Args> 实现参数的完美转发。这样,无论传入的类型具有什么样的构造函数,都能够完美执行placement new。
C++11中引入了变长参数模板的概念,来解决参数个数不确定的模板。
1 template<class... T> class Test {};
2 Test<> test0;
3 Test<int> test1;
4 Test<int,int> test2;
5 Test<int,int,long> test3;
6
7 template<class... T> void test(T... args);
8 test();
9 test<int>(0);
10 test<int,int,long>(0,0,0L);
以上分别是使用变长参数类模板和变长参数函数模板的例子。
2.1变长参数函数模板
T... args 为形参包,其中args是模式,形参包中可以有0到任意多个参数。调用函数时,可以传任意多个实参。对于函数定义来说,该如何使用参数包呢?在上文的MemNew中,我们使用std::forward依次将参数包传入构造函数,并不关注每个参数具体是什么。如果需要,我们可以用sizeof...(args)操作获取参数个数,也可以把参数包展开,对每个参数做更多的事。展开的方法有两种,递归函数,逗号表达式。
递归函数方式展开,模板推导的时候,一层层递归展开,最后到没有参数时用定义的一般函数终止。
1 void test()
2 {
3 }
4
5 template<class T, class... Args>
6 void test(T first, Args... args)
7 {
8 std::cout << typeid(T).name() << " " << first << std::endl;
9 test(args...);
10 }
11
12 test<int, int, long>(0, 0, 0L);
13
14 output:
15 int 0
16 int 0
17 long 0