转载

boost bind 源码剖析

C11/C14 出现了许多漂亮的特性,比如说auto, shared_ptr等。看到这些特性,相信用过boost很多年的童鞋心里暗暗一笑:哥已经爽了XX年了 :)

boost::bind就是boost库中一个很好玩的东西。

它能将各种不同签名的函数或者方法bind成你需要的签名的形式。简单的来说,就是一个bind1st, bind2nd的加强版。

我写这个笔记的目的不在于介绍它的使用。而是为了解开她的面纱,脱掉她的什么什么。因此我先假设大家对它的使用都很熟了。

考察下面一个有趣的例子:

void func(int){ } void use_func(boost::function<void (int, int) > f) {  f(1,2); } void test_bind2() {     use_func(boost::bind(&func, _1)); }  

它能通过编译吗?有运行错误吗?

带着这个问题,来看看它的源码。

boost::bind 要解决的问题

首先从这个问题开始,boost::bind是干嘛的,要解决什么问题呢?

它的本质就是预先把函数指针和参数(包括占位符参数和实际参数)存储起来,等到调用的时候再进行真正的函数调用。

说白了,它就是一个functor。在这句话里涉及到几个关键词,函数指针,参数的存储;调用。

很自然的,我们可以想象它必然是这个样子的:

namespace boost {  template<...> // 一大堆的模板,可能的样子是 template< template R, class F, class Arg1, class Arg2...>  class bind_t : public ... // 一些父类  {  public:   RETURN_TYPE operator() (Arg1 ag, ...) // 几个参数  private:   void* func_ptr;   Args1 arg;   Args2 ...;  }; }  

上面只是我们的猜测,真正的boost::bind可能跟我们想的不一样。但是它一定要包含:

1) 函数指针存储 ) --> 对应我们的Functor ;

2) 参数存储 (占位符,和实际参数) --> 对应我们的Args ...

3) 调用 --> operator()

实现

由于boost::bind是大神写的,肯定代码没有上面的那么丑。让我们一步一步来完善它。

参数存储

我们把参数用一个类型来存储起来,让代码更干净一点。不妨把这个参数类命名为:storage。由于boost::bind最多支持9个参数的情况,因此对应的,我们可以定义九个类。

template<class A1> struct storage1 { explicit storage1 (A1 a1) : a1_(a1) {} A1 a1_; };  template<class A1, class A2> struct storage2 : public storage1<A1> { storage2 (A1 a1, A2 a2) : stroage1<A1>(a1), a2_(a2) {} A2 a2_; }; ...// 省略storage3... storage9 

storage之间用继承可以省掉很多代码。而且会带来其他的方便。

占位符

但是,占位符怎么办呢?我们都知道在boost::bind里,用_1, _2 ...等几个占位符来表示第几个参数等待后续传入。这些占位符(_1, _2 ...)到底是什么呢? 在boost/bind/placeholders.hpp里面有这样的定义。

boost::arg<1> _1; boost::arg<2> _2; //... 

而boost::arg是一个简单的模板类型:

template <int I> struct arg {     arg() {}     ... }; 

这样,根据模板参数 的不同,就有了9个不同类型的变量。

加入占位符之后的存储

现在,有了占位符。本质上,它也是一个变量,对应着一个模板类型。为了区分占位符和普通数据类型,需要对storage做特殊处理。由于storage是一个模板类。因此,特殊处理的方案就是偏特化:

template <int I> struct storage1< boost::arg<I> > { ...// ??? } 

这样每个storage都对应这一个偏特化版本。问题是省略的地方(...)应该填入什么样的代码?

回到偏特化的目的上,偏特化的目的是要让之后函数调用的时候,能准确的根据参数类型(是占位符,还是实参),选择函数调用的参数。

既然这样,我们需要加入某种机制,使得两种类型,普通类型和特化类型分别返回不同的结果。

C++ trait是一种选择,比如我们可以在普通版本和偏特化版本中加入一个arg_type变量。分别指向不同的类型(T 和arg)。

但boost::bind却不是这样做的。 其成员变量A1 a_。就可以作为一种标志位。

因此这个偏特化版本的完整定义是:

template <int I> struct storage1< boost::arg<I> > {     explicit storage1( boost::arg<I> ) {}     static boost::arg<I> a1_() { return boost::arg<I>(); }     ...// 省略的部分代码与文档的逻辑无关 } 

这样我们就可以定义某种结构来取参数了。

比如在调用的时候,我们可以把传入的参数构造成某种类型,然后在参数调用的过程中,根据返回类型,决定使用a1_还是传入的参数。

注意到,如果该类型是个占位符,boost::bind将a_ 从变量变成了静态函数!达到了节省空间的目的。

现在我们有了一个参数的storage,不妨把它命名为storage1。而boost::bind支持九个参数,storage从一到九的过程是怎样的呢?继承和组合都能达到我们的目的。boost::bind采用的是继承,storage2继承storage1, storage3继承storage2... 以此类推。如下图示:

boost bind 源码剖析

采用继承的好处是不仅可以省下许多代码,而且对空间的节省也有一定的好处。

boost::bind将构造boost::bind时,和调用时传入参数"构造的类型"统一起来。

在构造时为存储参数和占位符构造一个storage;在取参数的时候需要传入真正的参数用以取代占位符,把这些参数再构造另一个storage。

当调用触发时,根据前一个storage(构造时)中参数的类型做不同处理:如果是占位符,则从后者中取参数;否则,从前者中取。

这样做,也是行得通的。但是boost::bind采取的方案是一种更优雅的办法。

boost::bind 采取的方案

既然有为占位符而生的boost::arg, 为什么不另外创建一个为普通类型而生的模板类呢?这个类是value 类。

template<class T> class value { public:  value(T const & t): t_(t) {}  T & get() { return t_; }  T const & get() const { return t_; }  bool operator==(value const & rhs) const  {   return t_ == rhs.t_;  } private:  T t_; };  

这样storage模板的类型可能是value, 也可能是boost::arg。

取参数的时候就比较简单了。

假设s_cont 表示构造boost::bind时创建的storage类,而s_call表示调用创建的类。为这个storage加上operator[] 操作符,以storage 1为例:

template <class A1> struct storage { A1 operator[] (boost::arg<1>) const { return a1_; } template<class T> T & operator[] (_bi::value<T> & v) const { return v.get(); } ... }; 

在参数调用的时候,我们只需要

s_call[s_cont::a1] 

就能拿到对应的类型。

参数获取

由于取参数的操作与存储无关,可以将这两个职责分离开来。定义listN类,分别接口继承自stroageN,并将operator[]操作符置于其中。

当然,listN不仅仅只有这一个职责,它还有另一个职责,这也是为什么开头部分的代码在调用的时候( f(1, 2) )虽然传入的参数个数不匹配,却能正确的调用的原因。

现在我们可以改写boost::bind,它应该有类似如下的定义:

template <class R, class F, class L> class bind { public:  bind_t(F f, L const & l): f_(f), l_(l) {}  ... private:  F f_;  L l_; };  

从引言开始的例子可以看出, boost::bind(&f, _1) 这个明明只有一个参数的Functor,能正确的转换成两个参数的。实际上,只要保证第一个参数(已有)类型匹配,它根本不关心后面跟了多少个参数。因此,可以猜测,boost::bind 至少实现了九种重载的operator。

  template<class A1> result_type operator()(A1 & a1) const   {    list1<A1 &> a(a1);    ...//   }   template<class A1, class A2> result_type operator()(A1 & a1, A2 & a2)   {    list2<A1 &, A2 &> a(a1, a2);    ...//   } ....  

省略的部分是什么样的代码?

现在,boost::bind 参数存储的部分真正的类间关系图如下:

boost bind 源码剖析

正确的调用

从引言部分的例子可以看到,尽管我们传入 f(10, 20) 以两个参数的方式去调用真正的函数,却没有出错。

这说明,boost::bind能根据函数真正需要参数的类型,而不是调用时传入的类型去匹配正确的调用。

而我们在使用boost::bind的时候,在构造bind的结构时,传入了正确的参数类型!

包含这些正确参数类型的参数保存在内部结构listN 中。因此,只要在listN中实现对应的operator 就能保证正确的调用:

template<class A1, class A2> result_type operator()(A1 & a1, A2 & a2) {     list2<A1 &, A2 &> a(a1, a2);     BOOST_BIND_RETURN l_(type<result_type>(), f_, a, 0); } 

把注意力放到最后一行代码,可以看到。构造boost::bind时候传入的前期绑定的参数信息保存在l_ 中,函数指针保存在f_ 中。真正调用时候传入的参数信息保存在a 中。有了这些信息,我们就能保证调用的正确性了。

template<class F, class A> void operator()(type<void>, F & f, A & a, int) {     unwrapper<F>::unwrap(f, 0)(a[base_type::a1_], a[base_type::a2_]); } ... 

统一入口

当然,可以看到,我们使用boost::bind的时候,传入的模板参数并不是template< class R, class F, class L> 这三个。因此boost::bind需要定义一系列的接口(对应着九个不同类型的参数个数),并最终返回正确的bind_t类型。下面是两个参数的一个例子:


template<class R, class B1, class B2, class A1, class A2> _bi::bind_t<R, BOOST_BIND_ST R (BOOST_BIND_CC *) (B1, B2), typename _bi::list_av_2<A1, A2>::type> BOOST_BIND(BOOST_BIND_ST R (BOOST_BIND_CC *f) (B1, B2), A1 a1, A2 a2) { typedef BOOST_BIND_ST R (BOOST_BIND_CC *F) (B1, B2); typedef typename _bi::list_av_2<A1, A2>::type list_type; return _bi::bind_t<R, F, list_type> (f, list_type(a1, a2)); }

它根据调用时候我们使用的参数推导出,函数返回值类型R, Functor类型 R (*f) (B1, B2),传入的参数类型 A1, A2。

总结

有了前面的这些分析,一些现象就很容易解释了。比如为什么支持 _2, _1 交换顺序等。

至于函数指针保存和调用的部分,代码比较简单;boost::bind为支持成员函数做了特殊处理,因为成员函数的调用实际上是instancePtr->foo(..),因此需要记录instancePtr信息。这部分代码在bind_mf_cc.hpp 和 mem_fn_template.hpp。

代码比较简单易读,这里就不多做分析了。

最后附上一张内部结构的图,作为本文的结束:

boost bind 源码剖析
正文到此结束
Loading...