CPP Bind

bind是一组用于函数绑定的模板。在对某个函数进行绑定时,可以指定部分参数或全部参数,也可以不指定任何参数,还可以调整各个参数间的顺序。对于未指定的参数,可以使用占位符_1、_2、_3来表示。_1表示绑定后的函数的第1个参数,_2表示绑定后的函数的第2个参数,其他依次类推。在使用bind之后,会生成一个新的函数对象作为返回值。bind类似于curry或者偏应用,但是功能上来说更为强大。

Bind使用

bind可以绑定到普通函数、函数对象、类的成员函数和类的成员变量这四种类型。下面的代码介绍了这几种类型bind的形式(代码来自C++ STL 2ed,55页):

#include <functional>
#include <memory>
void func(int x, int y)
{

}
auto l = [](int x, int y)
{

};
std::function<void(int, int)> func2 = [](int x, int y)
{

};

class C
{
public:
    void operator () (int x, int y) const
    {

    }
    void memfunc(int x, int y) const
    {

    }
    int memobj;
};
int main()
{
    C c;
    std::shared_ptr<C> sp(new C);
    int a;
    // bind() uses callable objects to bind arguments:
    std::bind(func, 77, 33)(); // calls: func(77,33)
    std::bind(l, 77, 33)(); // calls: l(77,33)
    std::bind(func2, 33, 44);//calls: func2(33,22)
    std::bind(func2, std::placeholders::_2, std::placeholders::_1)(22, 33);//calls func2(33,22)
    std::bind(C(), 77, 33)(); // calls: C::operator()(77,33)
    std::bind(c, 77, 33)(); // calls: C::operator()(77,33)
    std::bind(*sp, 77, 33)(); // calls: C::operator()(77,33)
    std::bind(&C::memfunc, c, 77, 33)(); // calls: c.memfunc(77,33)
    std::bind(&C::memfunc, sp, 77, 33)(); // calls: sp->memfunc(77,33)
    a=std::bind(&C::memobj,c)();//just return the c->*memobj
    a=std::bind(&C::memobj,std::placeholders::_1)(c);//return c->*memobj
}

在上面的代码中可以看出,std::bind有三种应用模式:

  • 对于普通函数、匿名lambda、std::function和重载了函数操作符的对象,直接以名称或对象值传入,这些类型最终都会退化为函数指针,退化机制为std::decay

  • 而对于类内部的函数,则需要获得函数的地址,然后传入类实例的指针;

  • 对于类内部成员的访问,也需要首先获得成员指针,然后再传入类实例的指针。这个功能看上去不像是函数调用,但是C++标准里面的确把这个定为可调用对象。

Bind主要任务

根据上面的代码说明,我们可以将std::bind的功能解构如下:

  • 识别占位符

  • 保存所有参数的类型和值;

  • 根据所绑定的可调用对象的类型选择不同的实现

  • 生成一个新的可调用对象

  • 被生成的可调用对象在接受实参时,根据占位符调整实参的顺序

  • 返回调用结果

下面我们就分别对这几个功能进行实现。

占位符实现

占位符是_1,_2的形式,不过这种形式只是他们的简写,其实真正的标识符为std::placeholders::_1,std::placeholders::_2。下面的代码就完美实现了一个placeholder,这里我们就不考虑名字空间的问题了。

template <int NUM> struct placeholder
{
};

template <typename T> struct is_placeholder;
template <int NUM> struct is_placeholder<placeholder<NUM> >
{
    enum
    {
        value = NUM
    };
};
template <typename T> struct is_placeholder
{
    enum
    {
        value = 0
    };
};

这里的struct is_placeholder判断模板参数的类型利用了模板的特化。由于占位符是从1开始的,所以0作为非占位符类型的值是可取的。这样,我们利用了这个模板类完成了编译期占位符的类型识别。看到这里,读者可能会问:_1,_2呢。不要着急,要开心,我下面给你吃啊。

extern placeholder<1> _1;
extern placeholder<2> _2;
extern placeholder<3> _3;
extern placeholder<4> _4;
extern placeholder<5> _5;
extern placeholder<6> _6;
extern placeholder<7> _7;
extern placeholder<8> _8;
extern placeholder<9> _9;
extern placeholder<10> _10;
extern placeholder<11> _11;
extern placeholder<12> _12;
extern placeholder<13> _13;
extern placeholder<14> _14;
extern placeholder<15> _15;
extern placeholder<16> _16;
extern placeholder<17> _17;
extern placeholder<18> _18;
extern placeholder<19> _19;
extern placeholder<20> _20;
extern placeholder<21> _21;
extern placeholder<22> _22;
extern placeholder<23> _23;
extern placeholder<24> _24;

要多少占位符,有多少占位符,管饱。

参数列表的保存

其实这个参数列表不仅仅可以保存bind参数的列表,而且可以保存绑定之后生成的可调用对象的实参列表。该参数列表可以直接采用std::tuple来保存,但是如何取出参数列表中的值就很麻烦了。因为最终在返回的可调用对象在执行函数调用的时候,实参顺序并不一定是从左到右的,同时内部还夹杂着被绑定的非占位符参数。为了得到所期望的参数顺序,我们需要利用下面的代码来实现:

template <int ...N> struct seq
{
};
template <unsigned N, unsigned...S> struct gen;
template <unsigned N, unsigned...S> struct gen : gen<N - 1, N - 1, S...>
{
};
template <unsigned...S> struct gen<0, S...>
{
    typedef seq<S...> type;
};

template <int N, typename B, typename C>
typename std::tuple_element<N,typename std::decay<B>::type>::type
select(std::false_type, B&& b, C&& c)
{
    return std::get<N>(b);
}

template <int N, typename B, typename C>
typename std::tuple_element
<
    is_placeholder<typename std::tuple_element<N,typename std::decay<B>::type>::type>::value - 1,
    typename std::decay<C>::type
>::type
select(std::true_type, B&& b, C&& c)
{
    return std::get<
        is_placeholder<typename std::tuple_element<N,typename std::decay<B>::type>::type>::value - 1
    >(c);
}

首先来解释一下seqgen这两个结构。seq这个模板类完完全全是用来存储类型来用的;而gen这个模板类是用来生成一个整数序列。如果我们人工来展开的话,可以得到这样的结果:gen<N>::typeseq<0,1,...,N-1>的类型是相同的。我们可以利用std::is_same来检查我们这个推论:

(std::is_same<gen<5>::type, seq<0,1,2,3,4> >::value)==1;

而剩下的select模板函数则负责了从两个参数列表中提取出最后的原始函数的应用参数列表。要想理解这一点,我们首先要明确:绑定参数的列表长度等于最后函数应用的参数个数。所以得到了绑定参数列表的长度N之后,我们就可以利用gen<N>::type来生成一个seq<0,1,...,N-1>的类型。通过这个seq,我们可以利用0,1,...N-1select<x>来获得最终的应用参数。这里select中的参数b代表的是std::bind中绑定的参数列表(包括占位符),c是绑定之后的函数在调用时的参数列表,B,C是各自的类型,都是std::tuple。在执行select<x>的调用时,我们还需要提供一个参数来表明B中的第x个参数的类型是否是占位符,即等价于

is_placeholder<std::tuple_element<x,std::decay<B>::type>::type>::value>0;

上面这条语句的值存入一个std::integral_constant<bool,bool a>类型中:如果是占位符,则a=true,该类型为std::true_type;否则a=false,该类型为std::false_type。这样通过模板的重载,我们实现了用select函数获得正确的函数调用参数:

  • std::get<x>(b)是占位符placeholder<a>的时候,返回std::get<a-1>(c)

  • std::get<x>(b)不是占位符的时候,直接返回std::get<x>(b)

函数返回值的推导

我们在生成绑定之后的可调用对象的时候,要为其生成函数签名。参数列表的类型我们在上节中已经解决了,而返回值的类型还没解决。为了推导出返回值的类型,我们可以使用下面的代码:

template <typename Fun> struct GetResult
{
    typedef typename std::enable_if<
        std::is_class<typename std::decay<Fun>::type>::value,
        typename GetResult<decltype(&Fun::operator())>::result_type
    >::type result_type;
};//如果Fun本身是一个类,则返回其函数操作符的返回值类型

template <typename R, typename... Args>
struct GetResult<R(Args...)>
{
    typedef R result_type;
};//简单函数
template <typename C, typename R, typename... Args>
struct GetResult<R(C::*)(Args...)>
{
    typedef R result_type;
};//类内部函数
template <typename C, typename R, typename... Args>
struct GetResult<R(C::*)(Args...)const>
{
    typedef R result_type;
};//类内部const函数
struct GetResult<R(C::*)>
{
    typedef R result_type;
};//类内部成员的指针

在上面的代码中,GetResult的作用就是编译期确定返回值的类型,唯一需要的就是被绑定的函数的类型。被绑定的函数如前文所说有四种类型:

  • 简单函数;

  • 对象对象类型(包括lambda对象,function对象和重载了函数操作符的对象);

  • 类内部的函数指针;

  • 类内部的成员指针。

其中,类内部函数是否是const的能继续细分。总的来说,这样我们可以得到绑定函数的返回值类型了。

绑定函数的生成

为了表示最后生成的绑定函数,我们利用bind_t类型来存储我们的std::bind绑定结果。该bind_t类型的基本定义如下:

template<typename F, typename... Args>
class bind_t
{
    typedef std::tuple<typename std::decay<Args>::type...> BindArgs;
    typedef typename std::decay<F>::type CallFun;
    enum class BindType
    {
        MemberFunction = 0, MemberObject = 1, Other = 2
    };

public:
    typedef typename GetResult<
        typename std::remove_pointer<
        typename std::remove_reference<F>::type
        >::type
    >::result_type result_type;

    bind_t(F fun, Args... args) :_fun(fun), _bindArgs(args...)
    {
    }
    template<typename... CArgs>
        result_type operator()(CArgs&&... c);
}

在这个bind_t类型中,使用BindArgs来存储std::bind中的参数列表,使用CallFun来存储绑定的函数的指针,result_type存储返回值的类型,函数操作符operator()来执行最后的实际调用(为了理解这些声明,读者应该了解一下std::decay)。此外,这里还使用BindType来表示绑定函数的类型:0为成员函数,1为成员对象,其他的为2。这个BindType的值是由下面的语句确定的(后文中并没有显示使用BindType,而是直接采用值):

std::is_member_function_pointer<CallFun>::value ? 0 : std::is_member_object_pointer<CallFun>::value ? 1 : 2;

而完整的函数操作符重载的定义如下:

template<typename... CArgs>
result_type operator()(CArgs&&... c)
{
    std::tuple<CArgs...> cargs(c...);
    return callFunc(
        std::integral_constant<
        int,
        std::is_member_function_pointer<CallFun>::value ? 0 : std::is_member_object_pointer<CallFun>::value ? 1 : 2
        >(),
        cargs,
        typename gen<
        std::tuple_size<BindArgs>::value - std::is_member_function_pointer<CallFun>::value
        >::type()
        );
}

虽然我们当前还没有给出callFunc的定义,但是从该函数的使用形式可以猜出其形式有三种:

template<typename T, int ...S>
result_type callFunc(std::integral_constant<int, 2>, T&& t, seq<S...>);
template<typename T, int ...S>
result_type callFunc(std::integral_constant<int, 1>, T&& t, seq<S...>);
template<typename T, int ...S>
result_type callFunc(std::integral_constant<int, 0>, T&& t, seq<S...>);

函数体的定义

现在是最重要的一步,定义callFunc的函数体。我们先从callFunc(<int, 2>, T\&\& t, seq<S...>)这个最简单的讲起,代码如下:

template<typename T, int ...S>
result_type callFunc(std::integral_constant<int, 2>, T&& t, seq<S...>)
{
    return _fun(
        select<S>(
        std::integral_constant<bool,
        is_placeholder<typename std::tuple_element<S, BindArgs>::type>::value != 0
        >(),
        _bindArgs,
        t)...
        );
}

这里我们要注意到最后一个闭括号前面的...,这代表生成一个参数包parameter_pack。对于S...中的每一个值Sselect<S>的结果都存在于该参数包中。每一次select<S>的结果都是_func的第S个调用参数(以0开始计数)。同时我们注意到,seq<S...>seq<0,1,...N-1>的形式。所以,在所有的select<S>执行完之后,我们就能得到所有的最终调用参数,并拥有正确顺序。

然后再来围观callFunc(<int, 0>, T\&\& t, seq<S...>),这个是类内部函数指针的绑定。该函数的代码如下:

template<typename T, int ...S>
result_type callFunc(std::integral_constant<int, 0>, T&& t, seq<S...>)
{
    return (
        select<0>(  std::integral_constant<bool,
        is_placeholder<
        typename std::tuple_element<0,BindArgs>::type
        >::value != 0
        >()_bindArgst)->*_fun
        )
        (
        select<S + 1>(
        std::integral_constant<bool,
        is_placeholder<
        typename std::tuple_element<S + 1,BindArgs>::type
        >::value != 0
        >(),
        _bindArgs,
        t
        )...
        );
}

此时参数bindArgs的第一个参数是this指针,最终函数调用形式为this->_func(realArgs),其中realArgs中参数的个数为bindArgs的个数减1。所以在获得最终参数时,需要往右偏移一个单位,所以是select<S+1>而不是select<S>

最后,callFunc(<int, 1>, T\&\& t, seq<S...>),这个绑定的是类内部的数据成员指针。代码如下:

template<typename T, int ...S>
result_type callFunc(std::integral_constant<int, 1>, T&& t, seq<S...>)
{
    return select<0>(
            std::integral_constant<bool,
        is_placeholder<
        typename std::tuple_element<0,BindArgs>::type
        >::value != 0
        >(),_bindArgs,t)->*_fun;
}

简而言之,就是一个this->*_fun,一个数据成员获取操作。

Bind实现

经过这么多的前奏,我们可以用下面的代码来实现bind:

template <typename F, typename... Args>
bind_t<typename std::decay<F>::type, typename std::decay<Args&&>::type...>
bind(F f, Args&&... args)
{
    return bind_t<
        typename std::decay<F>::type,
        typename std::decay<Args&&>::type...
    >(f, args...);
}

不过,总的来说,当前的一些实现有很多问题,主要是没使用完美转发std::forward。不过,作为原理解释,点都点到了。先将就着看吧,后期在慢慢修改。如果发现文中的错误,欢迎指出。

参考链接

Published:
2015-04-17 21:31
Category:
Tag:
CPP15