天天看點

手把手教你實作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。有興趣的小夥伴可以對模闆元程式設計進行深入的研究。