本文说明利用C++的模板的威力,自动实现字符串到函数调用的功能,可以极大简化这类工作的开发工作量
- 应用场景说明:
在一些小型软件系统中,可能需要自己实现一个远程调用的机制,简单的方法就是将要调用的函数以及参数通过字符串(或其他编码方式)传递给函数实现端,在函数实现端提供一个统一的函数原型来分析接受到的字符串参数,解码为需要的具体数据类型再转调用真正的功能实现函数,类似消息处理机制。在这之前一般建立函数名到包装函数指针的一个映射表,通过函数名找到函数指针。
一般的实现方式如下:
//真正需要远程调用的函数:
int Function1(int , long);
//包装的转调用函数:
std::string Wrapper(const std::string& input)
{
int ret = Function1(getFirstInt(input),getSecondLong(input));
//将返回值转换成字符串回传
...
}
函数映射表:
void* FuncMap[][2] = {
{ "Function1",&Wrapper},
...
};
- 原来方式的缺点:
主要是需要手工编写和各种具体的业务函数(上面的Function1)对应的包装函数(上面的Wrapper),当需要远程调用的函数数量比较大时,工作量也不小
下面提到的新技巧也是最大程度的简化这个工作量,利用下面的技巧,最后要实现远程调用只需要这样写:
FUNC_REGHELPER("Function1",&Function1); //it is all
也就是说参数的解码和函数名到函数指针的映射关系通过上面的宏FUNC_REGHELPER已经完全自动生成了,不但简化工作量,而且更容易保证代码质量。
- 实现方式
字符串到函数参数的解码工作在《ValueList的应用之一》中已经说明了其实现原理,这里利用其原理可以很容易实现我们自己的编解码的处理。后面就直接给出实现方式,不在具体说明。至于到函数名到函数指针的映射应该是很简单的,但是自动实现的难题是如何提取具体函数类型(返回类型,参数个数和类型),模板可以帮助我们做到这一点。
不管怎样,处理字符串的包装函数也是需要,这里用类来定义:
class Invoker
{
public:
virtual ~Invoker();
virtual std::string invoke(const std::string& input) = 0;
};
class InvokeDriver: public Invoker
{
std::map<std::string,Invoker*> m_invokers;
friend class InvokerRegister;
public:
static InvokeDriver* instance();
std::string invoke(const std::string& input);//搜索内部的MAP表,完成映射功能
};
从命令行上输入字符串调用的时候,内部简单调用InvokeDriver::instance()->invoke(input)就可以了。
现在的思路就是针对每个具体的函数类型实现一个Invoker的子类,在这个子类中实现函数参数的编解码工作,如下:
template<class RET,class P1>
class Invoker1: public Invoker
{
typedef RET(* FUNC_TYPE)(P1);
typedef ValueList<P1,null_type> PARAM_TYPE;
FUNC_TYPE m_func;
PARAM_TYPE m_param;
public:
Invoker1(FUNC_TYPE func): m_func(func){}
std::string invoke(const std::string& input)
{
decode(input,m_param);
std::string ret;
encode_single(ret,(*m_func)(m_param.m_v1);
return ret;
}
};
struct null_type{};
template<class T1,class T2>
struct ValueList
{
T1 m_v1;
T2 m_v2;
};
null_type仅仅是一个类型标识,表示ValueList的结束。当然ValueList实际上可以用std::pair来定义,含义一样。我们简单看看它的解码操作
template<class T1,class T2>
void decode(const std::string& input, ValueList<T1,T2>& values)
{
decode_single(values.m_v1);
decode(input,values.m_v2);
}
void decode(const std::string& input,null_type)
{
//do nothing
}
这样上面的定义就将多个参数的解码操作分解为单个参数的解码操作,我们可以提供基本类型的解码实现,对于用户自定义类型,那么请他们自己提供一个解码实现即可,而且也可以最终简化为一个宏定义,这里可以参考《ValueList的应用方式之一》中的介绍,就不重复了。
对于encode_single的实现和decode_single的实现也一样。
按照上面的定义方式我们可以提供没有参数,只有一个参数,两个参数。。。的Invoker的子类:
template<class RET> Invoker0;
template<class RET,class P1,class P2> Invoker2;
...
- 提供函数注册功能
函数注册功能就是要根据函数类型自动生成合适的Invoker子类类型,这里类的成员模板函数来实现:
class InvokerRegister
{
void doExecute(const std::string& name,Invoker* invoker);
public:
template<class RET>
InvokerRegister(const std::string& funcName,RET(*fun)())
{
doExecute(funcName,new Invoker0<RET>(fun));
}
template<class RET,class P1>
InvokerRegister(const std::string& funcName,RET(*func)(P1))
{
doExecute(funcName,new Invoker1<RET,P1>(func));
}
template<class RET,class P1,class P2>
InvokerRegister(const std::string& funcName,RET(*func)(P1,P2,P3))
{
doExecute(funcName,new Invoker2<RET,P1,P2>(func));
};
...
};
上面利用构造函数的重载机制来处理不同参数个数的函数对应的Invoker子类的注册,为了方便应用我们提供下面的宏:
#define JOIN_DIRECT(x,y) x##y
#define JOIN_LINE(x) JOIN_DIRECT(x,__LINE__)
#define FUNC_REGHELPER InvokerRegister JOIN_LINE(InvokerRegV)
这样我们这样使用:
FUNC_REGHELPER("Function1",&Function1);
实际上是定义了一个InvokerRegister的对象,参数是"Function1"和&Function1,根据后者类型会自动选择合适的构造函数,生成合适的Invoker的子类。这样当我们从命令行上输入:
Function1(1,100)
的时候,整个处理过程如下:
InvokeDriver::instance()中解析出函数名Function1,找到匹配的Invoker指针,实际类型是
template<class RET,class T1,class T2> class Invoker2;
它的实现中就会将输入参数"1,100"解析为ValueList<int,ValueList<long,null_type> >,然后完成转调用,而说的这些过程全是由FUNC_REGHELPER这个宏自动实现!!!
- 返回类型是VOID的处理
上面Invoker1的实现:
encode_single(ret,(*m_fun)(m_param.m_v1));
中如果函数的返回类型是void,则会出错,因此我们必须分辨出函数的返回类型是VOID和非VOID两种情况,这样我们就不能直接调用m_fun,可以考虑利用Functional的方式来实现,如下:
template<class RET>
struct FuncOP
{
template<class P1>
RET operation()(RET(*func)(P1), P1 p1)
{
return (*func)(p1);
}
...//同样处理其他不同参数个数的函数类型
};
对于VOID类型提供,如下的特化版本:
template<> struct FuncOP<void>
{
template<class P1>
null_type operator()(void(*func)(P1), P1 p1)
{
(*func)(p1);
return null_type;
}
...//同样处理其他不同参数个数的函数类型
};
这样Invoker1的实现中改为:
encode_single(ret,FuncOP<RET>(m_func,m_param.m_v1));
同时提供这样一个encode_single函数:
void encode_single(std::string& ret,null_type)
{
//do nothing
}
如此所有的问题都已经解决了,当我们完全实现了上面的代码的时候,对于新增的函数:
void newFunction(const char*,const char*);
只需要:
FUNC_REGHELPER("newFunction",&newFunction);
重新编译就可以使用了。
以上的实现,对于我们不习惯(或还没有养成)利用CPPUNIT(CUNIT)等类似机制实现自动化测试的时候,还可以通过它来支持手工测试,而且基本上没有什么新开发工作量。