前面说到可以通过反射机制来解放客户端对于同类型函数调用的手动分配,而将具体的定位和调用过程通过反射机制来实现,客户端输入的信息只需要是注册机中的function对应的key即可,这样可以在client程序中减少难看冗长的switch结构或if…else…结构。但是反射机制只是变相地将任务的复杂性交给了后台库的实现者,库的实现者依旧不得不挨个实现所有相似的函数个体。这里面重复的工作量可想而知,但是有什么办法可以减少这种工作量的浪费呢?
模板机制是个很好的切入点,可以让库的实现者从具体参数类型中解放出来,提供一个通用库和一系列通用的API接口。但是模板机制只是解放了参数类型,参数个数的维度依旧是不自由的,C++11推出的可变参数模板便是对此前模板机制的完善。原来的模板参数可以使得类和函数的参数类型“任意化”,如果再加上“参数个数的任意化”,则可以实现对于形参使用场景的无关性,这无疑将极大地提升库(类或函数)的复用性。下面先来依次介绍下模板、可变参数和它们两者的结合体可变参数模板。
废话不多说,直接来看一组代码对比,先来模板函数的使用场景。
//要实现两个任意类型参数比较,并返回较大值,
//考虑到基本数据类型有int,float, double,则显然Max要采用重载机制,
//考虑接口统一性,将返回值统一设置为double
double Max(int param1, int param2)
{
return param1 > param2 ? param1 : param2;
}
double Max(int param1, float param2)
{
return param1 > param2 ? param1 : param2;
}
double Max(int param1, double param2)
{
return param1 > param2 ? param1 : param2;
}
double Max(float param1, float param2)
{
return param1 > param2 ? param1 : param2;
}
double Max(float param1, double param2)
{
return param1 > param2 ? param1 : param2;
}
double Max(double param1, double param2)
{
return param1 > param2 ? param1 : param2;
}
这样子的代码看起来是不舒服的,那么这时候,模板函数的使用,将会将coder从这种繁杂重复的实现中解放出来。这种对比效果还是蛮直接的。
template<typename T, typename B>
double Max(T param1, B param2)
{
return param1 > param2 ? param1 : param2;
}
模板类的使用应该说是更为常见的,C++的STL容器中基本都是支持模板机制的,如STL::list
、STL::map
等。举个简单的动态数组模板类的实现代码
template <typename T>
class queue
{
public:
queue(void)
{
const int INIT_SIZE = 10;
this->mCapacity = INIT_SIZE;
this->mFront = this->mEnd = 0;
this->mpData = new T[this->mCapacity];
}
queue(const queue &from)
{
this->mCapacity = from.mCapacity;
this->mFront = from.mFront;
this->mEnd = from.mEnd;
this->mpData = new T[this->mCapacity];
for (int i=0; i<this->mCapacity; i++)
{
this->mpData[i] = from.mpData[i];
}
}
virtual ~queue(void)
{
if (this->mpData)
{
delete[] this->mpData;
}
this->mpData = 0;
}
virtual void push_back(T value)
{
if (this->size() >= this->mCapacity-1)
{
this->double_resize();
}
this->mpData[this->mEnd++] = value;
this->mEnd %= this->mCapacity;
}
virtual const T pop_front(void)
{
assert(!this->empty()); //输出断言,如果队列是空,则弹出断言,如果非空,则弹出队列首部
T tmp = this->mpData[this->mFront++];
this->mFront %= this->mCapacity;
return tmp;
}
virtual const T &get_front(void) const
{
assert(!this->empty());
return this->mpData[this->mFront];
}
virtual int size(void) const
{
return (this->mEnd+this->mCapacity-this->mFront) % this->mCapacity;
}
virtual bool empty(void) const
{
return this->size()==0;
}
const queue &operator =(const queue &from)
{
if (this->mpData)
{
delete[] this->mpData;
}
this->mpData = 0;
this->mCapacity = from.mCapacity;
this->mFront = from.mFront;
this->mEnd = from.mEnd;
this->mpData = new T[this->mCapacity];
for (int i=0; i<this->mCapacity; i++)
{
this->mpData[i] = from.mpData[i]; //此处可以看出队列的深拷贝还浅拷贝是由子元素自己实现的操作符函数的实现决定的
}
return *this;
}
protected:
virtual void double_resize(void)
{
T *tmp = new T[this->mCapacity*2];
int size = this->size();
for (int i=0; i<size; i++)
{
tmp[i] = this->mpData[(this->mFront+i)%this->mCapacity];
}
delete[] mpData;
this->mpData = tmp;
this->mCapacity*=2;
//this->mEnd = this->mFront + size;
//this->mFront = this->mFront;
this->mEnd = size;
this->mFront = 0;
}
int mFront,mEnd,mCapacity;
T *mpData;
};
可变参数的原理可以参考我的这篇文章,其实可变参数的需求并非近期才有的,在C语言时代并依旧存在对可变参数的支持,stdarg.h便是可变参数头文件。而关于可变参数最为经典的使用莫过于printf()。下面的代码段大致展示了可变参数的实现过程。
#define va_list char*
#define va_start(ap,arg) ( ap = (va_list)&arg + sizeof(arg))
#define va_arg(ap, t) ( *(t*) ( (ap+=sizeof(t)) - sizeof(t) ) )
#define va_end(ap) ( ap = (va_list) 0)
...
int printf(const char* format, ...)
{
va_list(arglist);
va_start(arglist, format);
return vfprintf(stdout, format, arglist);
}
可变参数的运行原理,其实说白了就是串联式遍历。那么可变参数模板呢?
可变参数模板机制说白了其实就是模板机制和可变参数机制的组合,从而实现函数和库的”参数类型和参数个数无关性“,这无疑将增加库的复用性。但是可变参数模板机制的使用其实还是需要考虑清楚的,因为其代码形式不便于理解,通过“递归脱壳”的方式进行参数处理,常用于共享库的制作,而在client程序中较少使用。
[这篇文章中关于可变参数模板]有很好的解释,来看代码
template<typename... Elements> class tuple;
template<typename Head, typename... Tail>
class tuple<Head, Tail...> : private tuple<Tail...> {
Head head;
public:
/* implementation */
};
template<>
class tuple<> {
/* zero-tuple implementation */
};
typename
是数据类型的通配符,除了typename
,还可以使用具体的数据类型int, unsigned, double
,只不过这时便只是可变参数的使用了,此外还可以使用class, struct
等狭义的通配符。...
则是可变参数机制的使用,便是可将输入的参数集合打包成一个数据包,该包的代称为Elements。这便是variadic template被称为“可变参数+模板”的原因。
前面说到,可变参数模板机制打包的数据包是匿名空间,故而显然不能采用数组类型的寻址,如Elements[0]
,而只能采用“递归”脱壳形式。既然是递归,那么必然存在一个递归出口,即递归终止条件和匹配的终止操作。
template<>
class tuple<> {
/* zero-tuple implementation */
};
上面的代码便是对应递归终止操作和出口,这个概念很好理解,只和递归有关还和可变参数模板无关。
下面来看下,可变参数模板机制下参数的几种使用方式
1. 模板数据包类型声明中, template <typename Head, typename... Tail>, 声明Tail为数据包类型别名
2. 函数或类中作为参数声明 double Max(Head first, Tail... rest)
3. 递归调用展开,作为形参输入 Max(rest...), 其中`...`代表unpack解包
4. 迭代器处理模式,对于当前数据包内所有元素执行操作 someOperation(rest)...
代码示例如下
//===========================================================
// FileName : another.cpp
// Author : ranger
// Version : 1.0
// Description : 用于演示C++11的可变参数模板功能variadic template
// IDE & Compiler info : codeblocks 16.01 & gnu gcc with g++ c++11 -std=c++11
//===========================================================
#include <iostream>
#include <sstream>
using namespace std;
//声明模板函数,用于后续对数据包内所有元素进行操作,在参数末尾添加“!!!”
template <typename T>
std::string debug_rep(const T& t)
{
std::ostringstream ret;
ret << t ;
ret << "!!!" ;
return ret.str();
}
//递归终止操作
template <typename T>
std::ostream& print(std::ostream& os, const T& t)
{
return os<<t;
}
//可变参数模板函数声明
template <typename T, typename... Args>
std::ostream& print(std::ostream& os, const T& t, const Args&... rest)
{
os<<t<<", ";
return print(os, rest...);
}
template <typename... Args>
std::ostream& errorMsg(std::ostream& os, const Args&... rest)
{
return print(os, debug_rep(rest)...); //手动使用模板包的unpack解包操作
}
int main()
{
int i(10);
std::string s("girls");
print(std::cout, i);
cout<<endl;
errorMsg(std::cout, i, s, 10, "ladies");
return 0;
}
前篇文章介绍了针对无返回者无形参类型函数
void func( )
的事件委托机制实现,并针对函数存在场外正常函数、类成员函数和类静态函数三类情况设置了原型模式来实现统一的接口。但是可以看到此前事件委托机制对于可委托的函数成员对象存在较大的限制,即要求无返回值无参,那么如果存在返回值存在个数类型不定的形参呢?
这种维度爆炸问题,显然是撞上了可变参数模板的枪口了。那么来看下如果通过可变参数模板机制来实现更为通用的事件委托机制。可以参考这篇文章的代码,超级好看的C++代码。