引子
最近群里比较热闹,大家都在山寨c++11的std::bind,三位童孩分别实现了自己的bind,代码分别在这里:
这些实现思路和ms stl的std::bind的实现思路是差不多的,只是在实现的细节上有些不同。个人觉得木头云的实现更简洁,本文中的简单实现中select函数用的是木头云的,在此表示感谢。下面我们来分析一下bind的基本原理。
bind的基本原理
bind的思想实际上是一种延迟计算的思想,将可调用对象保存起来,然后在需要的时候再调用。而且这种绑定是非常灵活的,不论是普通函数、函数对象、还是成员函数都可以绑定,而且其参数可以支持占位符,比如你可以这样绑定一个二元函数auto f = bind(&func, _1, _2);,调用的时候通过f(1,2)实现调用。关于bind的用法更多的介绍可以参考我博客中介绍。
要实现一个bind需要解决两个问题,第一个是保存可调用对象及其形参,第二个是如何实现调用。下面来分析如何解决这两个问题。
保存可调用对象
实现bind的首先要解决的问题是如何将可调用对象保存起来,以便在后面调用。要保存可调用对象,需要保存两个东西,一个是可调用对象的实例,另一个是可调用对象的形参。保存可调用对象的实例相很简单,因为bind时直接要传这个可调用对象的,将其作为一个成员变量即可。而保存可调用对象的形参就麻烦一点,因为这个形参是变参,不能直接将变参作为成员变量。如果要保存变参的话,我们需要用tuple来将变参保存起来。
可调用对象的执行
bind的形参因为是变参,可以是0个,也可能是多个,大部分情况下是占位符,还有可能占位符和实参都有。正是由于bind绑定的灵活性,导致我们不得不在调用的时候需要找出哪些是占位符,哪些是实参。如果某个一参数是实参我们就不处理,如果是占位符,我们就要将这个占位符替换为对应的实参。比如我们绑定了一个三元函数:auto f = bind(&func, _1, 2, _2);调用时f(1,3);由于绑定时有三个参数,一个实参,两个占位符,调用时传入了两个实参,这时我们就要将占位符_1替换为实参1,占位符_2替换为实参3。这个占位符的替换需要按照调用实参的顺序来替换,如果调用时的实参个数比占位符要多,则忽略多余的实参。
调用的实参,我们也会先将其转换为tuple,用于在后面去替换占位符时,选取合适的实参。
bind实现的关键技术
将tuple展开为变参
前面讲到绑定可调用对象时,将可调用对象的形参(可能含占位符)保存起来,保存到tuple中了。到了调用阶段,我们就要反过来将tuple展开为可变参数,因为这个可变参数才是可调用对象的形参,否则就无法实现调用了。这里我们会借助于一个整形序列来将tuple变为可变参数,在展开tuple的过程中我们还需要根据占位符来选择合适实参,即占位符要替换为调用实参。
根据占位符来选择合适的实参
这个地方比较关键,因为tuple中可能含有占位符,我们展开tuple时,如果发现某个元素类型为占位符,则从调用的实参生成的tuple中取出一个实参,用来作为变参的一个参数;当某个类型不为占位符时,则直接从绑定时生成的形参tuple中取出参数,用来作为变参的一个参数。最终tuple被展开为一个变参列表,这时,这个列表中没有占位符了,全是实参,就可以实现调用了。这里还有一个细节要注意,替换占位符的时候,如何从tuple中选择合适的参数呢,因为替换的时候要根据顺序来选择。这里是通过占位符的模板参数I来选择,因为占位符place_holder<I>的实例_1实际上place_holder<1>, 占位符实例_2实际上是palce_holder<2>,我们是可以根据占位符的模板参数来获取其顺序的。
bind的简单实现
#include <tuple> #include <type_traits> using namespace std; template<int...> struct IndexTuple{}; template<int N, int... Indexes> struct MakeIndexes : MakeIndexes<N - 1, N - 1, Indexes...>{}; template<int... indexes> struct MakeIndexes<0, indexes...> { typedef IndexTuple<indexes...> type; }; template <int I> struct Placeholder { }; Placeholder<1> _1; Placeholder<2> _2; Placeholder<3> _3; Placeholder<4> _4; Placeholder<5> _5; Placeholder<6> _6; Placeholder<7> _7; Placeholder<8> _8; Placeholder<9> _9; Placeholder<10> _10; // result type traits template <typename F> struct result_traits : result_traits<decltype(&F::operator())> {}; template <typename T> struct result_traits<T*> : result_traits<T> {}; /* check function */ template <typename R, typename... P> struct result_traits<R(*)(P...)> { typedef R type; }; /* check member function */ template <typename R, typename C, typename... P> struct result_traits<R(C::*)(P...)> { typedef R type; }; template <typename T, class Tuple> inline auto select(T&& val, Tuple&)->T&& { return std::forward<T>(val); } template <int I, class Tuple> inline auto select(Placeholder<I>&, Tuple& tp) -> decltype(std::get<I - 1>(tp)) { return std::get<I - 1>(tp); } // The invoker for call a callable template <typename T> struct is_pointer_noref : std::is_pointer<typename std::remove_reference<T>::type> {}; template <typename T> struct is_memfunc_noref : std::is_member_function_pointer<typename std::remove_reference<T>::type> {}; template <typename R, typename F, typename... P> inline typename std::enable_if<is_pointer_noref<F>::value, R>::type invoke(F&& f, P&&... par) { return (*std::forward<F>(f))(std::forward<P>(par)...); } template <typename R, typename F, typename P1, typename... P> inline typename std::enable_if<is_memfunc_noref<F>::value && is_pointer_noref<P1>::value, R>::type invoke(F&& f, P1&& this_ptr, P&&... par) { return (std::forward<P1>(this_ptr)->*std::forward<F>(f))(std::forward<P>(par)...); } template <typename R, typename F, typename P1, typename... P> inline typename std::enable_if<is_memfunc_noref<F>::value && !is_pointer_noref<P1>::value, R>::type invoke(F&& f, P1&& this_obj, P&&... par) { return (std::forward<P1>(this_obj).*std::forward<F>(f))(std::forward<P>(par)...); } template <typename R, typename F, typename... P> inline typename std::enable_if<!is_pointer_noref<F>::value && !is_memfunc_noref<F>::value, R>::type invoke(F&& f, P&&... par) { return std::forward<F>(f)(std::forward<P>(par)...); } template<typename Fun, typename... Args> struct Bind_t { typedef typename decay<Fun>::type FunType; typedef std::tuple<typename decay<Args>::type...> ArgType; typedef typename result_traits<FunType>::type result_type; public: template<class F, class... BArgs> Bind_t(F& f, BArgs&... args) : m_func(f), m_args(args...) { } template<typename F, typename... BArgs> Bind_t(F&& f, BArgs&&... par) : m_func(std::move(f)), m_args(std::move(par)...) {} template <typename... CArgs> result_type operator()(CArgs&&... args) { return do_call(MakeIndexes<std::tuple_size<ArgType>::value>::type(), std::forward_as_tuple(std::forward<CArgs>(args)...)); } template<typename ArgTuple, int... Indexes > result_type do_call(IndexTuple< Indexes... >& in, ArgTuple& argtp) { return simple::invoke<result_type>(m_func, select(std::get<Indexes>(m_args), argtp)...); //return m_func(select(std::get<Indexes>(m_args), argtp)...); } private: FunType m_func; ArgType m_args; }; template <typename F, typename... P> inline Bind_t<F, P...> Bind(F&& f, P&&... par) { return Bind_t<F, P...>(std::forward<F>(f), std::forward<P>(par)...); } template <typename F, typename... P> inline Bind_t<F, P...> Bind(F& f, P&... par) { return Bind_t<F, P...>(f, par...); }
测试代码:
void TestFun1(int a, int b, int c) { } void TestBind1() { Bind(&TestFun1, _1, _2, _3)(1, 2, 3); Bind(&TestFun1, 4, 5, _1)(6); Bind(&TestFun1, _1, 4, 5)(3); Bind(&TestFun1, 3, _1, 5)(4); }
bind更多的实现细节
由于只是展示bind实现的关键技术,很多的实现细节并没有处理,比如参数是否是引用、右值、const volotile、绑定非静态的成员变量都还没处理,仅仅供学习之用,并非是重复发明轮子,只是展示bind是如何实现, 实际项目中还是使用c++11的std::bind为好。null同学还图文并茂的介绍了bind的过程,有兴趣的童孩可以看看.
关于bind的使用
在实际使用过程中,我更喜欢使用lambda表达式,因为lambda表达式使用起来更简单直观,lambda表达式在绝大多数情况下可以替代bind。
如果你觉得这篇文章对你有用,可以点一下推荐,谢谢。
c++11 boost技术交流群:296561497,欢迎大家来交流技术。