淺談std::bind的實現

bind這個東西爭議不少,用起來很迷,並且不利於編譯優化,不少人都推薦用lambda而非bind。簡單說,bind就是經過庫抽象實現了lambda裏須要寫進語言標準的東西,變量捕獲,參數綁定,延遲求值等。可是以此帶來的缺陷就是,雖然bind生成的可調用對象的結構是編譯期肯定的,可是它的值,尤爲是被調用的函數,所有是在運行期指定的,而且可調用對象也只是一個普通的類,所以很難進行優化。除此以外,標準庫的bind實現,只提供了20個placeholder進行參數綁定,沒法擴展,這也是實現的一個坑。所以,在有條件的狀況下,應該使用lambda而非bind,lambda是寫入語言標準的特性,編譯器面對一個你寫的lambda,和bind生成的普通的對象相比,能夠更加清楚你想要作什麼,並進行鍼對性的優化。express

雖然說如此,bind怎麼實現的仍是很trick的,這篇文章就講一講bind的實現。數組

bind的使用app

bind的使用分兩步,第一步是生成可調用對象,使用你想要bind的東西和須要捕獲和延遲綁定的參數調用bind,生成一個新的callable。函數

std::string s;

auto f = mq::bind(&std::string::push_back, std::ref(s), mq::ph<0>);

這裏用的是我本身的實現,bind的第一個參數是你要綁定的callable,這裏是一個成員函數,後面的是用來調用的參數,由於是一個成員函數指針,因此參數的第一個應該是一個對象實例,這裏是一個引用包裝的字符串 std::ref(s) ,最後是一個placeholder,他表示對於生成的可調用對象,在調用時第0個參數要被傳到這裏。這裏和標準不同,標準的placeholder是從1開始的。測試

使用起來就是這樣的優化

f('a');
f('b');

這裏用來調用的參數就會被傳給綁定進去的push_back的第0個參數。spa

bind的實現指針

首先就是bind生成的對象,要作的就是把callable和後面傳的參數都丟進一個類裏面,這樣就構成了一個綁定對象,bind是這麼實現的,lambda的內部也是這麼實現的。生成的對象叫binder。code

template<class TFunc, class... TCaptures>
class binder
{
    using seq = std::index_sequence_for<TCaptures...>;
    using captures = std::tuple<std::decay_t<TCaptures>...>;
    using func = std::decay_t<TFunc>;

    func _func;
    captures _captures;
public:
    explicit binder(TFunc&& func, TCaptures&&... captures)
        : _func(std::forward<TFunc>(func))
        , _captures(std::forward<TCaptures>(captures)...)
    {
    }
    //...

這個實現至關的直接,func就是被綁定的函數,captures是一個tuple,裏面裝了bind調用時第1個參數後面的全部參數,構造函數把這些東西都forward進去存住。注意全部的類型參數都decay過,這是由於要去掉全部的引用,數組退化成指針,否則無法放進tuple。對象

而bind,簡單點,就是用調用的參數構造binder而已。

template<class TFunc, class... TCaptures>
decltype(auto) bind(TFunc&& func, TCaptures&&... captures)
{
    return detail::binder<TFunc, TCaptures...>{ std::forward<TFunc>(func), std::forward<TCaptures>(captures)... };
}

這裏用了C++14的decltype(auto)返回值,這個寫法就是經過return語句直接推斷返回類型,而且不作任何decay操做。

binder構造好了,下面就是構造它的operator()重載,函數簽名也是至關的直接:

//class binder
    template<class... TParams>
    decltype(auto) operator()(TParams&&... params);
};

接受不定數量的參數而已,這裏不一樣於標準的實現,我沒有用任何的SFINAE來作參數的限制,若是調用的參數有錯,那麼大概會出一大片編譯錯誤。

它的實現是這樣的,我把上面binder的實現再複製過來一份一塊兒看

template<class TFunc, class... TCaptures>
class binder
{
    using seq = std::index_sequence_for<TCaptures...>;
    using captures = std::tuple<std::decay_t<TCaptures>...>;
    using func = std::decay_t<TFunc>;

    func _func;
    captures _captures;
public:
    explicit binder(TFunc&& func, TCaptures&&... captures)
        : _func(std::forward<TFunc>(func))
        , _captures(std::forward<TCaptures>(captures)...)
    {
    }

    template<class... TParams>
    decltype(auto) operator()(TParams&&... params);
};

template<class TFunc, class... TCaptures>
template<class... TParams>
decltype(auto) binder<TFunc, TCaptures...>::operator()(TParams&&... params)
{
    return bind_invoke(seq{}, _func, _captures, std::forward_as_tuple(std::forward<TParams>(params)...));
}

這裏operator()的實現就是調用的bind_invoke,參數是什麼呢,一個index_sequence,以前綁定好的函數和捕獲參數,和這裏傳入的參數列表,參數列表也轉發成tuple,爲何要作成tuple呢,由於tuple好用啊,後面就看出來了。

bind_invoke得到了上面這一大坨,它來負責params和_captures正確的組合出來,拿來調用_func。

咱們想一下_func應該怎麼調用,這裏可使用C++17的invoke,invoke(_func, 參數1, 參數2, ...)

而這些參數1,參數2,是怎麼來的呢,回去看一下調用bind時的captures,若是這個capture不是placeholder,那麼這個就是要放進invoke的對應的位置,而若是是placeholder<I>,那麼就從params裏面取對應的第I個參數放進invoke的位置。

畫個圖就是這個樣子的:

image

那麼,怎麼實現這種參數的選擇呢,經過包展開

template<size_t... idx, class TFunc, class TCaptures, class TParams>
decltype(auto) bind_invoke(std::index_sequence<idx...>, TFunc& func, TCaptures& captures, TParams&& params)
{
    return std::invoke(func, select_param(std::get<idx>(captures), std::move(params))...);
}

bind_invoke的內部直接調用了標準的std::invoke,傳入了func,和後面的select_param包展開的結果,仔細看如下select_param的部分,這裏是每一個select_param對應一個captures的元素和一整個params tuple

image

那麼select_param的實現你們也基本能猜出來, 對於第一個參數是placeholder<I>的狀況,就返回後面的tuple的第I個元素,若是不是,那就返回它的第一個參數。

這裏須要注意,select_param是不能用簡單的重載的,由於對於

template<size_t I>
void foo(plaecholder<I>)

template<class T>
void foo(T)
這兩個重載,是不能正確區分 placeholder<I>和其餘參數的,須要用SFINAE過濾,而我選擇另外一種解法,用模板特化,這樣更好擴展。

template<class TCapture, class TParams>
struct do_select_param
{
    decltype(auto) operator()(TCapture& capture, TParams&&)
    {
        return capture;
    }
};

template<size_t idx, class TParams>
struct do_select_param<placeholder<idx>, TParams>
{
    decltype(auto) operator()(placeholder<idx>, TParams&& params)
    {
        return std::get<idx>(std::move(params));
    }
};
這是 do_select_param的實現(上)和它的一個特化版本(下),特化版本匹配了參數是placeholder的狀況。

select_param函數自己,就是轉發對do_select_param的調用而已

template<class TCapture, class TParams>
decltype(auto) select_param(TCapture& capture, TParams&& params)
{
    return do_select_param<TCapture, TParams>{}(capture, std::move(params));
}
這樣bind的實現基本上就完結了。還差一個placeholder沒提,這個實現也很簡單,就是

template<size_t idx>
struct placeholder
{
};
爲了方便,使用C++14的變量模板來節省一下平時寫 placeholder<0>{}的代碼

template<size_t idx>
constexpr auto ph = placeholder<idx>{};
那麼,bind的實現就基本完結了!

擴展支持嵌套bind

標準的bind是支持嵌套的,好比以下代碼

// nested bind subexpressions share the placeholders
auto f2 = std::bind(f, _3, std::bind(g, _3), _3, 4, 5);
f2(10, 11, 12); // makes a call to f(12, g(12), 12, 4, 5);
嵌套bind也要能夠共享調用時的placeholder,這個實現也很簡單,只要給上面的do_select_param再增長一個特化,對於參數是binder的類型,嵌套地調用它就行了

template<class TFunc, class... TCaptures, class TParams>
struct do_select_param<binder<TFunc, TCaptures...>, TParams>
{
    decltype(auto) operator()(binder<TFunc, TCaptures...>& binder, TParams& params)
    {
        return apply(binder, std::move(params));
    }
};
這裏使用了C++17的apply,就是用tuple的參數包去調用一個函數,若是你的STL尚未實現它,本身去cppreference抄一個實現也行。

至此,bind的實現就完成了,這個實現能夠經過cppreference上的全部測試代碼,我沒有作進一步的測試,若是有錯,歡迎在下面評論區指出,謝謝。

相關文章
相關標籤/搜索