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的位置。
画个图就是这个样子的:
那么,怎么实现这种参数的选择呢,经过包展开
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
那么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上的全部测试代码,我没有作进一步的测试,若是有错,欢迎在下面评论区指出,谢谢。