做一个,送一个——invoker的额外好处 我们注意到function的构造和赋值函数及其基类的构造和赋值函数都是模板函数,这是因为用户可能提供函数也可能提供函数模板,但最关键的还是,functiont提供一种能力:对于function<double(int)>类型的泛型函数指针,用户可以给它一个int(int)类型的函数——是的,这是可行且安全的,因为其返回值类型int可以安全的转型为double,而对于这种类型兼容性的检查就在上面分析的invoke静态成员函数中,这就是我们要说的额外好处——如果类型兼容,那么invoke函数就能正常编译通过,但如果用户给出类型不兼容的函数,就会得到一个错误,这个错误是在编译器实例化invoke函数代码的时候给出的,例如,用户如果这样写:
RT1 f(P1,P2); // RT1(P1,P2)函数类型,这里的RT1,P1,P2假定已经定义,这是一般化的符号
function<RT(P)> f_ptr; //RT(P)函数类型,同样假定RT,P已定义
f_ptr = &f; //类型不兼容,错误! |
这就会导致编译错误,错误发生在invoke静态成员函数中。下面我就为你解释为什么。
我想你对function_invoker1考的三个模板参数仍然心存疑惑,我们再一次来回顾一下其声明:
template<typename FunctionPtr,typename R,typename T0>
struct function_invoker1 |
我们还得把目光投向assign_to模板函数,其中使用function_invoker1的时候是这样的:
typedef typename detail::function::get_function_invoker1<
FunctionPtr,R,T0>::type invoker_type; |
这里,给出的FunctionPtr,R,T0三个模板参数将会原封不动的传给function_invoker1,那么对于我们上面的错误示例,这三个模板参数各是什么呢?
首先,我们很容易看出,FunctionPtr就是assign_to模板函数的模板参数,也就是用户传递的函数或仿函数的类型,在我们的错误示例中,函数f的类型为RT1(P1,P2),所以
| FunctionPtr = RT1(*)(P1,P2) |
而R,T0则是用户在实例化function模板时给出的模板参数,我们写的是function<RT(P)>,于是:
所以,对于我们的错误示例,invoker_type的类型为:
| function_invoker1< RT1(*)(P1,P2),RT,P> |
对于这样一个function_invoker1,其内部的invoke静态成员函数被实例化为:
static RT invoke(any_pointer function_ptr,P a0) { RT1 (*f)(P1,P2)= //FunctorPtr f =reinterpret_cast<RT1(*)(P1,P2)>(function_ptr.func_ptr); return f(a0); //错啦!瞧瞧f的型别,f接受一个P类型的参数吗?编译器在此打住。 //这行语句的另一个隐含的检查是返回值类型匹配,f(...)返回RT1,而invoke须得返回RT } |
看看最后一行语句,所有的检查都在那里了——我们最终把检查“委托”给了C++底层的类型系统。
很精妙不是吗?虽然在模板形式的assign_to函数中,看起来我们并不关心到底用户给的参数是何类型,看起来用户可以把任何函数或仿函数塞过来,但是一旦下面触及invoker的赋值,就得实例化invoke静态成员函数,其中的:
一下就把问题暴露出来了!这种把类型检查延迟到最后,不得不进行的时候,由C++底层的类型系统来负责检查的手法的确很奇妙——看起来我们没有在assign_to函数中及时利用类型信息进行类型检查,但是我们却并没有丧失任何类型安全性,一切最终都逃不过C++底层的类型系统的考验!
function如何对待成员函数 对于成员函数,assign_to的重载版本只有一行:
| this->assign_to(mem_fn(f)); |
mem_fun(f)返回一个仿函数,它封装了成员函数f,之后一切皆与仿函数无异。
关于mem_fun的细节,这里就不多说了,大家可以参考STL中的实现,相信很容易看懂,这里只简单的提醒一下,成员函数封装的效果是这样的:
| R (C::*)(T0,T1,...) --> R (*)(C*,T0,T1,...) 或 R (*)(C&,T0,T1,...) |
safe_bool惯用手法 如你所知,对于函数指针fptr,我们可以这样测试它:if(fptr) ...,所以function也应该提供这一特性,然而如果直接重载operator bool()则会导致下面的代码成为合法的:
function<int(int)> f;
bool b=f; |
这显然不妥,所以function用另一个巧妙的手法替代它,既所谓的safe_bool惯用手法,这在function定义内部的源码如下:
struct dummy { void nonnull(){};};
typedef void (dummy::*safe_bool)(); //确保safebool不能转型为任何其它类型!
operator safe_bool () const
{ return (this->empty())? 0 : &dummy::nonnull; } |
这样,当你写if(f)的时候,编译器会找到operator safe_bool(),从而将f转型为safe_bool,这是个指针类型,if语句会正确判定它是否为空。而当你试图把f赋给其它类型的变量的时候则会遭到编译期的拒绝——因为safe_bool无法向其它类型转换。
get_function_tag<>用于萃取出函数所属类别(category),各个类别在源代码中已经列出,至于它到底是如何萃取的,这与本文关系不是很 大,有一点需要提醒一下:函数指针类型也是指针类型,这听起来完全是句废话,但是考虑这样的代码:
template<typename T> struct is_pointer{enum{value=0};};
template<typename T> struct is_pointer<T*>{enum{value=1};};
std::cout<<is_pointer<int(*)(int)>::value; //这将输出 1 |
也就是说int(*)(int)可以与T*形式匹配,匹配时T为int(int)。
最后一些细节 1. 我没有给出function_base的源代码,实际上那很简单,它最主要的成员就是union any_pointer型的数据成员
| detail::function::any_pointer functor; //用于统一保存函数指针及仿函数对象指针 |
2. 我没有给出functor_manager的信息,实际上它与function的实现没有太大关系,它负责copy和delete函数对象,如果必要的话。所以我将它略去,它的源码在:”boost/function/function_base.hpp”里。
3. 我给出的源代码是将宏展开后的版本,实际的代码中充斥着让人眼花缭乱的宏,关于那些宏则又是一个奇妙的世界。Boost库通过那些宏省去了许多可见代码量。随着函数参数的不同,那些宏会扩展出function2,function3...各个版本。
本文只研究了int(int)型的情况,其它只是参数数目的改变而已。经过宏的扩展,function的偏特化版本将有:
template<typename R,typename Allocator>
class function<R(),Allocator>:public function0<R,Allocator>
{...};
template<typename R,typename T0,typename Allocator>
class function<R(T0),Allocator>:public function1<R,T0,Allocator>
{...};
template<typename R,typename T0,typename T1,typename Allocator>
class function<R(T0,T1),Allocator>:public function2<R,T0,T1,Allocator>
{...}; |
等更多版本,一共有BOOST_FUNCTION_MAX_ARGS+1个版本,BOOST_FUNCTION_MAX_ARGS为一个宏,表示最多能够接受有多少个参数的函数及仿函数对象,你可以重新定义这个宏为一个新值,以控制function所能支持的函数参数个数的最大值。其中的function0,function1,function2等名字也由宏扩展出。
阅读关于
C++ 的全部文章