手把手教你实现boost::bind

时间:2023-03-09 13:20:49
手把手教你实现boost::bind

前言

boost::bind操作想必大家都使用过,它特别神奇,能够绑定函数与参数,绑定后能够改变参数数量,并且还可以使用占位符。它可以绑定普通函数也可以绑定类成员函数。好多小伙伴试图看过boost::bind的源码,但是可能效果不佳。原因在于boost::bind的代码考虑了很多使用情况,而且还要兼容各种编译环境,所以实现的代码很复杂,很容易在看源码的时候被各种宏定义带跑偏,以至于乱了思路。在这里我试图抽出boost::bind核心骨架,适当简化,达到简单可理解的目的。

本文通过一个简单的例子逐渐引出bind的模板化,并探讨其参数列表、bind_t、以及必要的萃取函数的实现。本文所展示的代码,只用于探讨交流之用,其中固然有很多不足,请勿直接搬到线上。本文末尾有我抽取的bind骨架代码的github地址,只有300行左右,看起来更容易理解。好了,废话不多说,用一小段代码代码先展示一下boost::bind的神奇。

class Calculator {
public:
Calculator(){}
int add(int a, int b) {
return a + b;
}
};
int add(int a, int b) {
return a + b;
}
int main() {
//绑定普通函数
int a = boost::bind(add, 1, 2)(); //a = 3
a = boost::bind(add, _1, _2)(1, 2); //a = 3
//绑定类成员函数
a = boost::bind(&Calculator::add, &caltor, 1, 2)(); //a = 3
a = boost::bind(&Calculator::add, _1, _ 2, _3)(&caltor, 1, 2); //a = 3
return 0;
}

boost::bind的设计思想

如何绑定函数与参数

熟悉C/C++的小伙伴都知道,有一种数据类型叫做函数指针,在C里面经常使用函数指针调用函数,比如下面的例子。

int add(int a, int b) {
return a + b;
} //定义一个参数为两个int型,返回值为int型的函数指针类型F
typedef int (*F)(int, int);
//函数指针指向具体的add函数
F f = add;
//执行f(),就相当于执行add
int a = f(1, 2); // a = 3;

我们很自然的想到可以用结构体把函数指针以及参数存储到一起,这样一个函数执行需要的东西都在这个结构体里了。

struct bind_t {
bind_t(F f, int a, int b):f_(f),a_(a),b_(b) {}
F f_;
int a_;
int b_;
};

现在存放好了函数指针与参数,但是好像少了点什么,我们要是能像调用函数一样执行bind_t()就好了,很幸运我们可以用重载操作符()来完成。

struct bind_t {
//...
int operator()(){
return f_(a_ , b_);
}
//...
}; bind_t bnd(add, 1, 2);
int a = bnd(); //调用重载的操作符()后,a = 3;

现在的bind_t就好像是函数一样,达到了以假乱真的地步。但还是不够简洁,我们再额外封装一下。

bind_t bind(F f, int a, int b) {
return bind_t(f, a, b);
} int a = bind(add, 1, 2)(); // a = 3

现在,这看起来有点像boost::bind的了,但其实还差的远。我们只实现了对特定类型的函数的bind操作,可实际编程中我们面对的是不同参数数目,不同返回值的函数。不过不要怕,基本思想我们已经知道了,如下两点。

  • 利用结构体bind_t存放函数指针以及参数
  • 对bind_t进行运算符重载,重载()运算符,可以传入0,1,2...等数目的参数

现在我们尝试对bind_t进行模板化,bind_t需要的信息有函数的返回类型(operator()必须有返回值类型),函数类型,以及参数列表,那么我们可以这么写。

//R 返回值类型 F 函数类型 L 参数列表
template<class R, class F, class L>
struct bind_t {
bind_t(F f, L l): f_(f),l_(l){}
//执行时传入0个参数
R operator(){ 暂时先忽略return }
template<class A1>
//执行时传入1个参数
R operator()(A1 a1) { 暂时先忽略return }
//执行时传入2个参数
template<class A1, class A2>
R operator()(A1 a1, A2 a2) { 暂时先忽略return }
F f_;
L l_;
};

现在我们来思考一下如何实现bind_t的各个不同参数数目的operator()操作。

两种不同时态的参数列表

使用boost::bind的时候,我们可能需要在两个阶段传入函数执行时需要的参数

  • 绑定时:在调用boost::bind()函数时,除了需要给出函数指针,还需要给出一些必要参数,可确定的参数直接传参数值,不确定的参数传占位符。
  • 运行时:在执行bind_t的具体某个operator()时传入的参数,这些参数都是具体值,没有占位符。

bind_t在构造函数中传入的参数列表为绑定时参数列表,执行operator()时传入的参数列表为运行时参数列表。绑定时参数列表中的参数数量与函数定义所需的参数数目相同,只不过元素分为具体参数值和占位符两种。运行时参数列表与函数定义所需的参数数目不一定相同,它的大小为函数定义所需的参数数目N减去已经确定参数值的参数个数M,即N-M个。

实现bind_t的operator()

现在我们在bind_t的operator()中调用f_()怎么样?很遗憾有个问题,虽然bind_t中有绑定时参数列表,但是bind_t并不知道这个参数列表有多长,长度信息只有具体的参数列表本身知道。所以调用f_()时,bind_t不知道要传给f_()几个参数。当然这不是绝对的,可以用某些方法萃取出参数长度,然后通过多个if判断长度来硬编码,但是这个方法不优雅。

综上,我们放弃在bind_t的operator()中调用f,而是通过调用bind_t的成员l_上的某些操作来实现,因为只有l_是绑定时参数列表,它知道函数f_真正的参数个数。那么我们也重载L的operator()吧,bind_t调用l_()并传入相应的函数指针以及运行参数,当然也可以用个其它函数名代替。到这里bind_t的operator()应该大概这样实现。

template<R> struct type{};
template<class R, class F, class L>
struct bind_t {
//......
//执行时传入0个参数
R operator(){
list0 l0;
return l_(f_, l0);
} //执行时传入1个参数
template<class A1>
R operator()(A1 a1) {
list1<A1> l1(a1);
//这个是错误的,编译器在调用具体的l_()时无法确认R的类型,从而找不到具体调用的函数
//return l_(f_, l1);
//这个是对的,显示告诉编译器R的类型
return l_(type<R>(), f_, l1);
}
//执行时传入2个参数
template<class A1, class A2>
R operator()(A1 a1, A2 a2) {
list2<A1,A2> l2(a1, a2);
return l_(type<R>(), f_,l2);
}
//....
}

bind_t通过调用绑定时参数列表的operator()操作,传入函数指针f_和运行时参数列表,这样l_本身其实已经具备了执行f_的所有信息:绑定时参数、运行时参数、函数指针。

如何实现参数列表

好了,到这里所有问题都集中在了参数列表上,我们看看如何实现参数列表。 参数列表并不像我们认识到的STL中的list或者vector,因为STL中的list和vector中的元素都是同一种类型,而我们需要的是元素类型可能都不同的list。这就迫使我们颠覆直观上的list或者是vector,自己实现一个存储任意类型元素的list,可以用模板实现。由于bind_t调用list的operator()完成具体的函数f的调用,所以list必须有重载operator()。

class <int I> struct arg{};//占位符

//存放0个元素
struct list0 {
template<class T> T operator[](T v) { return v; } //传递参数值则直接返回参数值本身
template<class R, class F, class L> R operator()(typede<R>, F f, L l){ return f();}
};
//存放1个元素
template<class A1> struct list1{
list1(A1 a1): a1_(a1) {}
A1 operator[](arg<1>) { return a1_;} //传递占位符则返回本身存储的具体值
template<class T>
T operator[] (T t) { return t;} //传递参数值则直接返回参数值本身 //这个是错的,因为这会在bind_t中调用l_()时找不到实例化的模板,因为选择具体的operator()时,编译器无法确定类型R是什么。
/*
template<class R, class F, class L>
R operator()(F f, L l) {
return f(l[a1_]);
}*/
//为了使编译器知道R的类型,必须显示通过参数传入type<R>(),让编译器从而推导出R的类型。
template<class R, class F, class L>
R operator()(type<R>, F f, L l) {
return f(l[a1_]); //绑定时参数列表知道函数参数个数
} A1 a1_;
};
//存放两个元素
template<class A1, class A2> struct list2{
list2(A1 a1, A2 a2): a1_(a1), a2_(a2) {} A1 operator[](arg<1>) { return a1_; } //传递占位符则返回本身存储的具体值
A2 operator[](arg<2>) { return a2_; } //传递占位符则返回本身存储的具体值
template<class T>
T operator[] (T t) { return t;} //传递参数值则直接返回参数值本身 template<class R, class F, class L>
R operator() (type<R>, F f, L l) {
return f(l[a1_], l[a2_]); //绑定时参数列表知道函数参数个数
} A1 a1_;
A2 a2_;
};

为了支持任意长度的列表,我们可能需要照葫芦画瓢的实现listN,但是想想我们的用途:用作函数参数列表。如果你的程序符合规范,那么函数参数个数绝对不会太多,比如9个参数基本就满足需求,这样我们只需要实现list0~list9,本文只实现到list2,足以讲清楚原理。

此外,对于每个列表我们还模拟数组实现了operator[],像用下标访问数组一样访问列表。只不过下标传的不是坐标,而是具体的占位符arg<1>()或者arg<2>()。

我们巧妙的利用了绑定时list知道参数个数来到达到传给f多少个参数的目的,所以调用bind_t的构造函数时,必须保证给出的绑定时参数个数与函数定义的参数个数相同。

在给f传递参数的时候,我们利用了list的operator[],把绑定时参数列表的各个参数作为运行时参数列表的下标,从而调用f时传递的参数的都是值而不是占位符,并且根据占位符选择不同的参数顺序,我们举个例子看的更清楚一些。

list2 = [a1_= 1, a2_ = arg<1>() ]  //绑定时参数列表
list1 = [a1_ = 2] //运行时参数列表 list1 [ list2.a1_ ] = 1//由于list2.a1_是具体值而不是占位符,所以直接返回list2.a1_ = 1
list1 [ list2.a2_ ] = 2//由于list2.a2_是占位符而不是具体值,所以返回list1 [ arg<1>() ] = list1.a1_ = 2 //绑定时参数列表调用函数f并融合运行时参数列表获取适当参数传递给f
f(list1[list2.a1_], list1[list2.a2_]) <=> f(1, 2); //等价于调用f(1, 2) //再看一个例子
list2 = [a1_= arg<2>(), a2_ = arg<1>() ] //绑定时参数列表
list1 = [a1_ = 1, a2_ = 2] //运行时参数列表 list1 [ list2.a1_ ] = 2//由于list2.a1_是占位符,所以返回 list1 [ arg<2>() ] = list1.a2_ = 2
list1 [ list2.a2_ ] = 1//由于list2.a2_是占位符,所以返回 list1 [ arg<1>() ] = list1.a1_ = 1
//绑定时参数列表调用函数f并融合运行时参数列表获取适当参数传递给f
f(list1[list2.a1_], list1[list2.a2_]) <=> f(2, 1); //等价于调用f(2, 1),虽然运行时参数顺序为<1, 2>,但是实际传递给函数f的参数顺序为<2, 1>,这是由绑定时参数列表的占位符顺序< _2, _1>决定的,_1 = arg<1>(),_2 = arg<2>()

到此为止,我们讲述的boost::bind的最最基本的两个数据类型:bind_t以及参数列表,理论上可以使用一下。

int add(int a, int b) {
return a +b;
} bind_t<int, int (*)(int, int), list2<int, int> > bnd2(add, 1, 2);
int a = bnd2(); //a = 3;
bind_t<int, int(*)(int, int), list2<int, arg<1> > > bnd22(add, 1, arg<1>());
a = bnd22(2); // a = 3;

但是,这样使用实在是不方便,需要显示的指定返回值类型,函数类型,以及参数类型。我们利用模板自动萃取各种类型,达到简化的目的。

//萃取无参函数
template<class R>
bind_t<R, R (*)(), list0> bind(R (*f)()) {
list0 l0;
return bind_t<R, R (*)(), list0>(f, l0);
}
//萃取参数个数为1的函数
template<class R, class B1, class A1>
bind_t<R, R (*)(B1), list1<A1> > bind(R (*f)(B1), A1 a1) {
list1<A1> l1(a1);
return bind_t<R, R (*)(B1), list1<A1> >(f, l1);
}
//萃取参数个数为2的函数
template<class R, class B1, class B2, class A1, class A2>
bind_t<R, R (*)(B1, B2), list2<A1, A2> > bind(R (*f)(B1, B2), A1 a1, A2 a2) {
list2<A1, A2> l2(a1, a2);
return bind_t<R, R (*)(B1, B2), list2<A1, A2> >(f, l2);
} arg<1> _1;
arg<2> _2;
int a = bind(add, 1, 2)(); // a = 3
int b = bind(add, _1, 2)(1); //b = 3
int c = bind(add, 1, _1)(2); //b=3
int d = bind(add, _1, _2)(1, 2); // d= 3

这样,我们的使用方法就基本和boost::bind一毛一样了。

类成员函数的绑定

类成员函数的绑定与普通函数的绑定原理是一样的,不同的地方在于:调用类成员函数时,第一个参数一般总是某个具体对象的指针或者引用,这就导致了模板在支持类成员函数的参数个数时比普通函数的参数个数少一个。例如:boost::bind支持普通函数不多于9个参数的函数调用,而类成员函数最多支持8个参数。

为了能够像调用普通函数一样调用类成员函数,我们可以把类成员函数封装成一个仿函数,重载其operator()方法,使其能够像调用普通函数一样调用类成员函数。

//类T的一个函数返回值类型为R参数个数为0的类成员函数的仿函数
template<class R, class T> class mf0 {
public:
typedef R result_type;
typedef T * argument_type;
private: typedef R ( T::*F) ();
F f_; template<class U> R call(U & u, T const *) const {
return (u.*f_)();
} public: explicit mf0(F f): f_(f) {}
//限制第一个参数必须为类实例指针
R operator()(T * p) const {
return (p->*f_)();
} template<class U> R operator()(U & u) const {
U const * p = 0;
return call(u, p);
} R operator()(T & t) const {
return (t.*f_)();
}
};
//类T的一个函数返回值类型为R参数个数为1,且参数类型为A1的类成员函数的仿函数
template<class R, class T, class A1> class mf1 {
public: typedef R result_type;
typedef T * first_argument_type;
typedef A1 second_argument_type; private: typedef R ( T::*F) (A1);
F f_; template<class U, class B1> R call(U & u, T const *, B1 & b1) const {
return (u.*f_)(b1);
} public: explicit mf1(F f): f_(f) {}
//限制第一个参数必须为类实例指针
R operator()(T * p, A1 a1) const {
return (p->*f_)(a1);
} template<class U> R operator()(U & u, A1 a1) const {
U const * p = 0;
return call(u, p, a1);
} R operator()(T & t, A1 a1) const {
return (t.*f_)(a1);
}
}; //和普通函数一样,为了使用时避免手动填写各种参数类型,利用模板自动萃取
//萃取成员函数参数个数为0的成员函数
template<class R, class T, class A1>
bind_t<R, mf0<R, T>, list1<A1> >
bind(R (T::*f) (), A1 a1) {
typedef mf0<R, T> F;
typedef list1<A1> list_type;
return bind_t<R, F, list_type>(F(f), list_type(a1));
}
//萃取成员函数参数个数为1的成员函数
template<class R, class T,
class B1,
class A1, class A2>
bind_t<R, mf1<R, T, B1>, list2<A1, A2> >
bind(R (T::*f) (B1), A1 a1, A2 a2) {
typedef mf1<R, T, B1> F;
typedef list2<A1, A2> list_type;
return bind_t<R, F, list_type>(F(f), list_type(a1, a2));
} class Calculator {
public:
int add10(int a) {
return a + 10;
}
}; Calculator caltor;
int a = bind(&Calculator::add10, _1, _2)(&caltor, 1); // a = 11
int b = bind(&Calculator::add10, &caltor, _1)(1); // a = 11

到此,类成员函数的bind操作讲解完毕。

对仿函数的bind

以上基本实现了大部分操作,包括对普通函数、成员函数的bind与萃取,对于仿函数还没有对应的bind,我们加上对仿函数的bind。

template<class R, class F> bind_t<R, F, list0> bind(F f) {
typedef list0 list_type;
return bind_t<R, F, list_type>(f, list_type());
} template<class R, class F, class A1>
bind_t<R, F, list1<A1> >
bind(F f, A1 a1) {
typedef list1<A1> list_type;
return bind_t<R, F, list_type> (f, list_type(a1));
} template<class R, class F, class A1, class A2>
bind_t<R, F, list2<A1, A2> >
bind(F f, A1 a1, A2 a2) {
typedef list2<A1, A2> list_type;
return bind_t<R, F, list_type>(f, list_type(a1, a2));
} struct Addop {
int add(int a, int b) {
return a + b;
}
}; Addop op;
//必须显示指定仿函数的返回值类型
int a = bind<int>(op, _1, _2)(1, 2); // a = 3

注意:对于仿函数,我们处理的并不正确,因为没有考虑到仿函数的引用。即:如果执行的仿函数的操作能够改变仿函数内部的某些值,我们实现的bind操作并不完全正确。

boost::bind的引用问题

关于bind的引用,有很多值得玩味的地方,比如上例的仿函数。再比如绑定时传的参数如果是引用类型会有什么问题?比如如下代码:

int add10(int &a) {
a = a + 10;
return a;
} int a = 1;
boost::bind(add10, a)();
printf("a = %d\n", a); //a = 1 or a = 11?

利用boost绑定执行的结果与我们直接调用函数得到的结果是否完全相同?篇幅问题我们就不继续展开了,有兴趣的小伙伴可以想想如何解决这个问题。

boost::bind源码阅读

boost的实现要比本文实现复杂的多,因为它考虑到了各种使用场景。小伙伴们在阅读源码的时候要注意,如果理顺了如下几个结构体,将会很有用。

  • storageXXX:参数列表基类
  • listXXX:继承自storageXXX,参数列表
  • arg{}:占位符
  • value{}:具体值 //想想本文说的参数有两种类型,具体值和占位符,作者对这两个概念进行了封装
  • list_av_xxx:为了方便同时使用value以及arg建立一个list
  • bind_t:类似本文的bind_t
  • 萃取函数:为了方便针对不同数目的参数的函数进行bind,通过模板萃取进行自动匹配,分别在bind_cc.hpp和bind_mf_cc.hpp中

到此为止,我们介绍完了boost::bind的基本实现原理,为了使小伙伴能够随时查看本文的样例代码,我把简化代码放入到github上:https://github.com/haolujun/Encapsulation/blob/master/bind.cpp ,只有300行,支持最多3个参数的函数绑定,我相信大部分人都能看懂。

总结

boost::bind的实现可以说极具技巧性,很多地方都值得研究。模板编程比较抽象,因为我们一般的编程都是运行在具体硬件上,理解了计算机组成原理就可以写代码。但是模板编程是运行在编译器上,本质是利用模板的特殊语法促使编译器自动生成相对应的代码,而这方面的研究是很小众的,但是这一小撮人经常能弄出一些非常漂亮的东西,比如boost。有兴趣的小伙伴可以对模板元编程进行深入的研究。