C++11繫結器bind及function機制

2022-10-31 06:01:14

前言

之前在學muduo網路庫時,看到陳碩以基於物件程式設計的方式,大量使用boost庫中的bindfunction機制,如今,這些概念都已引入至C++11,包含在標頭檔案<functional>中。

本篇文章主要梳理C++繫結器相關的內容以及C++11中引入的function機制,其中繫結器主要有三種:bind1stbind2ndbind(C++11)。學完本篇內容,將對C++繫結器及function機制等的底層實現有深刻理解,那麼我們開始說吧。

函數物件

首先說說函數物件,之所以說函數物件,是因為繫結器、function都涉及到該部分概念。函數物件實際上是類呼叫operator()()小括號運運算元過載,實現像在「呼叫函數」一樣的效果,因此還有個別名叫「仿函數」。函數物件範例程式碼如下:

class Print {
public:
    void operator()(string &s) { cout << s << endl; }
};

int main() {
    string s = "hello world!";
    Print print; //定義了一個函數物件print
    print(s);
    return 0;
}

上面程式碼print(s);語句,看似像函數呼叫,其實是類物件print呼叫其小括號運運算元過載print.operator(string &s)print就是一個函數物件,至此對函數物件就有了基本的認識。

剖析繫結器bind1st、bind2nd

瞭解了函數物件,接下來我們說說繫結器,為什麼需要繫結器?在使用STL時經常會遇到STL演演算法中需要傳遞某元函數物件,比如在寫sort時,第三個引數決定了我們的排序規則,用來接收一個「比較器」函數物件,該函數物件是一個二元的匿名函數物件,形如greator<int>()或者less<int>()。二元函數物件的意思是,這個函數物件的小括號運運算元過載函數接收兩個引數,那麼幾元就表示接收幾個引數。下面是庫中自帶的greaterless模板類的原始碼實現,可以看到是對小括號運運算元過載的實現,sort第三個引數接收該模板類的二元匿名函數物件。

  template<typename _Tp>
    struct greater : public binary_function<_Tp, _Tp, bool>
    {
      _GLIBCXX14_CONSTEXPR
      bool
      operator()(const _Tp& __x, const _Tp& __y) const
      { return __x > __y; }
    };

  template<typename _Tp>
    struct less : public binary_function<_Tp, _Tp, bool>
    {
      _GLIBCXX14_CONSTEXPR
      bool
      operator()(const _Tp& __x, const _Tp& __y) const
      { return __x < __y; }
    };

再回到剛才的問題,那為什麼需繫結器?由於STL介面的限制,有時我們拿到的函數物件和特定STL演演算法中要接收的函數物件在引數上並不匹配,意思就是需要傳遞一個一元函數物件,你有一個二元函數物件,那可以通過繫結器提前繫結二元函數物件的其中一個引數,使得最終返回的是一個一元函數物件,那麼從二元函數物件到一元函數物件的轉換過程,就需要繫結器去實現。

如STL中的泛型演演算法find_if,可用來查詢可變長陣列vector中符合某個條件的值(這個條件比如是要大於50,要小於30,要等於25等等)。其第三個引數需要傳遞一個一元函數物件,假如現在要找到第一個小於70的數,可將繫結器與二元函數物件結合,轉換為一元函數物件後傳遞給find_if

我們知道系統自帶的greater<int>()less<int>()模板類物件是二元匿名函數物件,所以需要通過繫結器將其轉換為一元函數物件,可以通過bind1stbind2nd去繫結,顧名思義,前者對二元函數物件的第一個引數進行繫結,後者對二元函數物件的第二個引數進行繫結,兩個繫結器均返回一元函數物件,用法如下:

sort(vec.begin(), vec.end(), greater<int>()); //從大到小對vector進行排序
find_if(vec.begin(), vec.end(), bind1st(greater<int>(), 70));
find_if(vec.begin(), vec.end(), bind2nd(less<int>(), 70));

兩個繫結器分別提前繫結了一個引數,使得二元函數物件+繫結器轉換為一元函數物件:

operator()(const T &val)
greater a > b ====> bind1st(greater<int>(), 70) ====> 70 > b
less    a < b ====> bind2nd(less<int>(),    70) ====> a < 70

下面給出bind1st繫結過程圖,二元函數物件繫結了第一個數為70,變為一元函數物件,傳遞給find_if泛型演演算法,此時find_if所實現的功能就是:找出有序降序陣列中第一個小於70的數,所以find_if返回指向65元素的迭代器:

file:///Users/guochen/Notes/docs/media/16656563650484/16657214749366.jpg

以上就是繫結器的概念。因此需要繫結器的原因就很明顯了,繫結器可以返回一個轉換後的某元函數物件,用於匹配泛型演演算法

根據上面的理解,接下來實現一下bind1st,程式碼實現如下:

/*可以看到 自己實現的繫結器本質上也是個函數物件 呼叫operator()進行繫結*/
template<typename Compare, typename T>
class _mybind1st {
public:
    _mybind1st(Compare comp, T first) : _comp(comp), _val(first) {}
    bool operator()(const T &second) {
        return _comp(_val, second);
    }
private:
    Compare _comp;
    T _val;
};

/*實現bind1st 函數模板*/
//直接使用函數模板,好處是可以進行型別推演
template<typename Compare, typename T>
_mybind1st<Compare, T> mybind1st(Compare comp, const T &val) { //繫結器返回值_mybind1st為一元函數物件
    return _mybind1st<Compare, T>(comp, val);
}

上述程式碼中mybind1st繫結器第一個引數Compare comp是要繫結的二元函數物件,第二個引數val是在原有函數物件上繫結的值,最後繫結器呼叫_mybind1st模板函數物件的小括號運運算元過載並返回該一元匿名函數物件,可以看到_mybind1st小括號運運算元過載中已將繫結器mybind1st第二個引數val傳遞給了原本的二元函數物件Compare comp,因此原本系結器接收的二元函數物件只需要處理第二個引數。所以繫結器返回的函數物件_mybind1st其實是在原本的函數物件上套了一層引數的新的函數物件,閱讀上面的程式碼實現,就可更深刻的理解bind1st的底層原理。

與此同時,不難寫出bind2nd的實現,顧名思義該繫結器是對第二個引數進行繫結,不過多贅述,貼出實現程式碼:

template<typename Compare, typename T>
class _mybind2nd {
public:
    _mybind2nd(Compare comp, T second) : _comp(comp), _val(second) {}
    bool operator()(const T &first) {
        return _comp(first, _val);
    }
private:
    Compare _comp;
    T _val;
};

template<typename Compare, typename T>
_mybind2nd<Compare, T> mybind2nd(Compare comp, const T &val) {
    return _mybind2nd<Compare, T>(comp, val);
}

根據上文,我們清楚瞭解到泛型演演算法find_if第三個引數接收一元函數物件,且該泛型演演算法功能是尋找第一個符合某條件的元素,我們對其補充實現,程式碼貼出:

/** 
 * 自己實現了find_if後發現其實繫結器返回的就是繫結後的函數物件
 * 使用繫結器的目的:就是將原本某元的函數物件轉化為另一個元的函數物件
 * 說白了,繫結器還是對函數物件的一個應用
 **/
template<typename Iterator, typename Compare>
Iterator my_find_if(Iterator first, Iterator last, Compare comp) {
    for(; first != last; ++first) {
        if(comp(*first)) { //呼叫comp的小括號運運算元過載 一元函數物件 comp.operator()(*first)
            return first;
        }
    }
    return last;
}

此時要尋找vector中第一個小於70的數,就可以這樣寫:

auto it = my_find_if(vec.begin(), vec.end(), mybind1st(greater<int>(), 70));
cout << *it << endl; //列印vec中第一個小於70的數值

以上,圍繞bind1stbind2nd以及函數物件等,展開討論了繫結器bind1stbind2nd的實現原理,但是同時我們也發現其缺點,就是隻能對二元函數物件進行繫結轉換,讓其轉換為一元函數物件,那如果遇到很多元的函數物件,我們還得一個一個自己去實現嗎?所以將boost庫的boost::bind引入到了C++11標準庫中,接下來我們介紹C++11的繫結器std::bind,它是對上述兩種繫結器的泛化。支援任意函數物件(其實標準庫中最多支援29元函數物件,不過這也足夠使用了)。

補充:上面都是以函數物件為例,作為繫結器第一個引數傳遞,其實第一個引數可以是函數物件、成員函數、也可以是普通函數。

總結:繫結器本身是函數模板,繫結器第一個引數可能是普通函數、成員函數或函數物件等,返回的一定是函數物件。還有就是這兩個繫結器在C++17中已移除,因此僅用於學習和理解繫結器,也方便我們對C++11引入的bind的學習。至於當前這兩個繫結器如何實現對類成員函數的繫結等等我們也沒必要去尋找答案了(我一開始也在努力尋找如何使用這兩個繫結器去繫結類成員函數,但是發現bind可以很輕鬆地做到,當然如果大家知道怎麼使用bind1stbind2nd繫結類成員函數,也可以評論告知我,感謝~)。

C++11 bind通用繫結器(函數介面卡)

我們可將bind函數看作是一個通用的函數介面卡,它接受一個可呼叫函數物件,生成一個新的可呼叫函數物件來「適應」原物件的參數列。bind相比於bind1st和bind2nd,實現了「動態生成新的函數」的功能。簡言之,可通過bind函數修改原函數並生成一個可以被呼叫的物件,類似於函數的過載,但是我們又不需要去重新寫一個函數,用bind函數就可以實現。相信在上面講bind1st和bind2nd時,大家對這些關於繫結器(函數介面卡)的概念已經有所認知,我們直接看看如何用的吧。

繫結一個普通函數和函數指標

#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
int fun(int a, int b, int c, int d, int e) {
    return a + b - c + d - e;
}
int main() {
   int x = 1, y = 2, z = 3;
   auto g = bind(fun, x, y, _2, z, _1); //第一個引數&可省略 但最好寫成&fun
   cout << g(11, 22) << endl; // fun(1, 2, 22, 3, 11) => 1+2-22+3-11
   // cout << bind(fun, x, y, _2, z, _1)(11, 22) << endl; //等價
}

g是有兩個引數的二元函數物件,其兩個引數分別用預留位置placeholders::_2placeholders::_1表示,_2代表二元函數物件的第二個引數22_1代表二元函數物件的第一個引數11。這個新的可呼叫物件將它自己的引數作為第三個和第五個傳遞給fun,fun函數的第一個、第二個第四個引數分別被繫結到給定的值xyz上。

繫結一個類的靜態成員函數與繫結全域性函數沒有任何區別,這裡不做說明,可參考文章:[