C++ 并不是一门支持函数式编程的语言,但它可以通过函数对象这个概念在一定程度上模仿函数式编程的方式。在很多情况下,往往需要将某个函数的部分参数进行提前绑定后形成一个新的函数以供使用,例如我们需要遍历一个容器,将其中小于某个指定值的元素删除,这里就需要一个函数对象,接受一个参数,并判断此参数是否小于指定值。在通常情况下我们是有一个元素类型的大小比较的函数,但它接受的是两个参数,这就需要一个接口适配,这就带来了挑战。幸而我们有绑定技术,比如 boost::bind,则可以将其中一个参数进行绑定,从而将接受两个参数的函数转化为只接受一个参数的函数对象(注意是对象,它是一个类的对象)。
函数对象在这里是一个核心概念,它是重载了 operator() 函数的类的对象,从而它表现得像个函数一样可以进行传参并调用。而绑定技术就是在函数对象内部保存原始函数的指针,并将被绑定的参数也保存起来,在使用这个对象调用的时候则将保存的参数取出来与传入的参数一起传递给原始函数完成调用。例如就前面提到的问题,可以将大小比较函数(假如是小于)的第二个参数绑定为指定值得到一个函数对象,此时函数对象内部保存了这个指定值,而第一个参数则是使用被称为“占位符”的东西所占据,在后面进行调用这个函数并传递一个参数的时候,它使用传入的函数作为原始函数的第一个实参,而将内部保存的参数作为原始函数的第二个实参,然后调用原始函数,完成大小比较并返回 bool 值以供作是否删除的依据。
绑定原理大致如此,但现在我更感兴趣的是如何实现,虽然我没有看过 boost::bind 的实现代码(事实上是看过但没看懂),但照着上面的思路还是实现了一个简单版本出来,本文接下来就是讲解实现过程,以接受两个参数的函数为例。
我们需要一个“占位符”之类的东东,这东西主要是在构造函数对象的时候指定原始函数的哪些参数是需要在调用函数的时候传递的,而剩下的参数则是需要在构造函数时进行绑定的。它的实现没有什么内容,内部就是一个整数,指明它占的是第几个参数,但由于我们需要在编译期知道这个索引,所以使用了模板:
template<int index>
class place_holder
{
public:
static const int place_index = index;
};
place_holder<1> _1;
place_holder<2> _2;
上面顺便定义了两个占位符,分别用来占第一个参数的位置和第二个参数的位置。然后我们需要在函数对象内部保存原始函数的指针,所以我们在 bind 的内部有如下定义:
template<typename RT, typename T1, typename T2>
class bind
{
typedef RT (*fun_type)(T1, T2);
fun_type f;
};
其中 RT 是函数返回值类型,T1 与 T2 分别是两个参数的类型。接下来考虑要在函数对象内部保存绑定的参数,所以需要一个表示参数的东东,而且这个参数也可能是通过占位符表示的,所以有:
template<typename T>
class arg_item
{
T val;
bool holder;
public:
arg_item(const T& v, bool h) : val(v), holder(h) {}
T value() {return val;}
bool isholder() {return holder;}
};
成员 holder 代表此参数是否是用占位符代替的,而 val 则代表参数的值.接下来考虑如何在函数对象内保存参数列表.我们专门写一个容器来容纳参数列表,为了将 arg_item<T> 都放入一个容器里,我们需要arg_item<T> 都有同一类型(T不同则类型则不同),所以我们先给 arg_item 添加一个基类,以便在容器里放置基类的指针:
class arg_item_base
{
public:
virtual ~arg_item_base() {}
};
而 arg_item<T> 现在就要从这个基类进行继承了:
template<typename T>
class arg_item : public arg_item_base
{
T val;
bool holder;
public:
arg_item(const T& v, bool h) : val(v), holder(h) {}
T value() {return val;}
bool isholder() {return holder;}
};
而我们参数容器,则可以实现如下:
template<typename T1, typename T2>
class arg_list
{
std::vector<arg_item_base*> arg_container;
public:
arg_list(T1 p1, T2 p2)
{
arg_container.push_back(new arg_item<T1>(p1, false));
arg_container.push_back(new arg_item<T2>(p2, false));
}
arg_list(const place_holder<1>&, T2 p2)
{
arg_container.push_back(new arg_item<T1>(T1(), true));
arg_container.push_back(new arg_item<T2>(p2, false));
}
arg_list(T1 p1, const place_holder<2>&)
{
arg_container.push_back(new arg_item<T1>(p1, false));
arg_container.push_back(new arg_item<T2>(T2(), true));
}
arg_list(const place_holder<1>&, const place_holder<2>&)
{
arg_container.push_back(new arg_item<T1>(T1(), true));
arg_container.push_back(new arg_item<T2>(T2(), true));
}
~arg_list()
{
for (std::size_t i = 0; i < arg_container.size(); ++i)
{
delete arg_container[i];
}
}
};
这里要注意构造函数中,如果第一个参数使用占位符,则必须使用 place_holder<1>,如果第二个参数使用占位符,则必须使用 place_holder<2>,千万不能乱来,并且对于占位符的情况,相应的 arg_item<T> 的构造函数第二个参数一定要传递 true,以代表该参数是使用占位符表示的。这里 arg_item 都是通过 new 在堆上进行分配的,这显然性能不佳,后面再讲如何将它们放到栈上来,这里先以讲原理为主。
接下来我们就可以考虑在 bind 里放置一个 arg_list<T1, T2> 的成员以代表参数列表,但是在后面进行调用的时候需要从参数列表里取出某些参数,所以 arg_list 需要提供这样的接口,给一个索引,就给出相应位置上的参数项 arg_item<T>,所以添加一个 get 成员函数,但由于各参数项的实际类型并不相同(参数类型T不一样),所以 get 函数的返回类型需要做一个处理,为参数的索引和对应的参数类型作一个关联,于是 arg_list<T1, T2> 修改如下:
template<typename T1, typename T2>
class arg_list
{
std::vector<arg_item_base*> arg_container;
public:
template<int index>
struct arg_index_type_traits;
template<>
struct arg_index_type_traits<1>
{
typedef T1 value_type;
};
template<>
struct arg_index_type_traits<2>
{
typedef T2 value_type;
};
public:
arg_list(T1 p1, T2 p2)
{
arg_container.push_back(new arg_item<T1>(p1, false));
arg_container.push_back(new arg_item<T2>(p2, false));
}
arg_list(const place_holder<1>&, T2 p2)
{
arg_container.push_back(new arg_item<T1>(T1(), true));
arg_container.push_back(new arg_item<T2>(p2, false));
}
arg_list(T1 p1, const place_holder<2>&)
{
arg_container.push_back(new arg_item<T1>(p1, false));
arg_container.push_back(new arg_item<T2>(T2(), true));
}
arg_list(const place_holder<1>&, const place_holder<2>&)
{
arg_container.push_back(new arg_item<T1>(T1(), true));
arg_container.push_back(new arg_item<T2>(T2(), true));
}
~arg_list()
{
for (std::size_t i = 0; i < arg_container.size(); ++i)
{
delete arg_container[i];
}
}
public:
template<int index>
inline arg_item<typename arg_index_type_traits<index>::value_type>*
get()
{
typedef typename arg_index_type_traits<index>::value_type arg_type;
return dynamic_cast<arg_item<arg_type>*>(arg_container[index - 1]);
}
};
到了这里,准备工作就做的相当足够了,剩下的事就是实现 bind 了,这个不是什么难事,如下:
template<typename RT, typename T1, typename T2>
class bind
{
typedef RT (*fun_type)(T1, T2);
fun_type f;
arg_list<T1, T2> para_list;
public:
bind(fun_type fun, T1 p1, T2 p2) : f(fun), para_list(p1, p2) {}
bind(fun_type fun, const place_holder<1>& ph1, T2 p2) : f(fun), para_list(ph1, p2) {}
bind(fun_type fun, T1 p1, const place_holder<2>& ph2) : f(fun), para_list(p1, ph2) {}
bind(fun_type fun, const place_holder<1>& ph1, const place_holder<2>& ph2) : f(fun), para_list(ph1, ph2) {}
RT operator()()
{
assert(!para_list.get<1>()->isholder());
assert(!para_list.get<2>()->isholder());
return f(para_list.get<1>()->value(), para_list.get<2>()->value());
}
RT operator()(T1 r1)
{
assert(para_list.get<1>()->isholder());
assert(!para_list.get<2>()->isholder());
return f(r1, para_list.get<2>()->value());
}
RT operator()(T2 r2)
{
assert(!para_list.get<1>()->isholder());
assert(para_list.get<2>()->isholder());
return f(para_list.get<1>()->value(), r2);
}
RT operator()(T1 r1, T2 r2)
{
assert(para_list.get<1>()->isholder());
assert(para_list.get<2>()->isholder());
return f(r1, r2);
}
};
然后写点测试代码测测看:
int test(int n, bool b)
{
return b ? ++n : --n;
}
int main()
{
bind<int, int, bool> add_fun(test, _1, true);
std::cout << "add_fun(10) = " << add_fun(10) << std::endl;
bind<int, int, bool> de_fun(test, _1, false);
std::cout << "de_fun(10) = " << de_fun(10) << std::endl;
bind<int, int, bool> u_fun(test, _1, _2);
std::cout << "u_fun(10, true) = " << u_fun(10, true) << std::endl;
std::cout << "u_fun(10, false) = " << u_fun(10, false) << std::endl;
bind<int, int, bool> ua_fun(test, 10, true);
std::cout << "ua_fun() = " << ua_fun() << std::endl;
bind<int, int, bool> ud_fun(test, 10, false);
std::cout << "ud_fun() = " << ud_fun() << std::endl;
return 0;
}
不出意外的话,你将看到如下输出:
add_fun(10) = 11
de_fun(10) = 9
u_fun(10, true) = 11
u_fun(10, false) = 9
ua_fun() = 11
ud_fun() = 9
到现在为止,一切似乎都工作的很好,原则性问题已经解决,但还有若干问题需要解决,首先是性能的问题,因为在保存参数列表的时候使用了堆内存,这个怎么都是不可接受的,要使用栈上的空间也不是难事,且看重新实现的 arg_list:
template<typename T1, typename T2>
class arg_list
{
std::vector<arg_item_base*> arg_container;
arg_item<T1> a1;
arg_item<T2> a2;
void init()
{
arg_container.push_back(&a1);
arg_container.push_back(&a2);
}
public:
template<int index>
struct arg_index_type_traits;
template<>
struct arg_index_type_traits<1>
{
typedef T1 value_type;
};
template<>
struct arg_index_type_traits<2>
{
typedef T2 value_type;
};
public:
arg_list(T1 p1, T2 p2) : a1(p1, false), a2 (p2,false)
{
init();
}
arg_list(const place_holder<1>&, T2 p2) : a1(T1(), true), a2(p2, false)
{
init();
}
arg_list(T1 p1, const place_holder<2>&) : a1(p1, false), a2(T2(), true)
{
init();
}
arg_list(const place_holder<1>&, const place_holder<2>&) : a1(T1(), true), a2(T2(), true)
{
init();
}
~arg_list(){}
public:
template<int index>
inline arg_item<typename arg_index_type_traits<index>::value_type>*
get()
{
typedef typename arg_index_type_traits<index>::value_type arg_type;
return dynamic_cast<arg_item<arg_type>*>(arg_container[index - 1]);
}
};
咋一看,似乎参数有重复保存的问题,但要注意那个 vector 里保存的是指针,所以这并不是问题,而且避免了堆内存的申请和释放,所得远远大于所失啊。
接着有一个更为严重的问题,就是上述实现对正确使用的情况没有问题,但如果我的绑定与我的调用不匹配,比如我第一个参数使用了占位符,而在调用的时候却给了一个布尔值(意图让其传递给第二个参数),这明显是绑定意图与调用意图不相匹配,但程序却能运行下去,只是在给原始函数传递第一个参数的时候,由于是按照占位符的情况构造的 arg_item<int>,里面保存的 val 是一个默认值0,从而导致运行结果不是我们期望的结果!这显然是一个致命的问题,我们希望在绑定与调用不相匹配的情况下让程序出错,可以是运行时崩溃,最好是连编译都通不过。
本人只讲让其运行时崩溃的办法,叙述如下,从 bind 的 operator() 实现可以看出,对于绑定的参数,是通过 arg_item<T> 的 value() 成员取出它之前保存的值,而如果这个参数当初是用占位符形式构造的,是不应当有对 value() 的调用的,于是希望能对用真实值构造的 arg_item 与用点位符构造的 arg_item 做一个类型上的区分,让用占位符构造的 arg_item 直接就没有 value() 成员函数,于是将 arg_item 添加一个模板参数,以指示该参数是否用点位符:
template<typename T, bool isholder>
class arg_item;
template<typename T>
class arg_item<T, false> : public arg_item_base
{
T val;
public:
static const bool isholder = false;
arg_item() {}
arg_item(const T& v) : val(v){}
T value() {return val;}
};
template<typename T>
class arg_item<T, true> : public arg_item_base
{
public:
static const bool isholder = true;
};
相应的 arg_list 也要做出调整:
template<typename T1, typename T2>
class arg_list
{
std::vector<arg_item_base*> arg_container;
arg_item<T1, true> ap1;
arg_item<T1, false> a1;
arg_item<T2, true> ap2;
arg_item<T2, false> a2;
public:
template<int index>
struct arg_index_type_traits;
template<>
struct arg_index_type_traits<1>
{
typedef T1 value_type;
};
template<>
struct arg_index_type_traits<2>
{
typedef T2 value_type;
};
public:
arg_list(T1 p1, T2 p2) : a1(p1), a2 (p2)
{
arg_container.push_back(&a1);
arg_container.push_back(&a2);
}
arg_list(const place_holder<1>&, T2 p2) : a2(p2)
{
arg_container.push_back(&ap1);
arg_container.push_back(&a2);
}
arg_list(T1 p1, const place_holder<2>&) : a1(p1)
{
arg_container.push_back(&a1);
arg_container.push_back(&ap2);
}
arg_list(const place_holder<1>&, const place_holder<2>&)
{
arg_container.push_back(&ap1);
arg_container.push_back(&ap2);
}
~arg_list(){}
public:
inline arg_item_base* get(int index)
{
return arg_container[index - 1];
}
};
可以看出,每个参数保存了两个,一个是用真实值绑定的,一个是用占位符的,但 vector 内却是用了哪个保存哪个,而另外 get 的接口也做了一个调整,因为在这里它并不知道里面保存的是 arg_item<T, true> 还是 arg_item<T, false>,这个由后面调用的时候去处理。
而 bind 的实现也应做出相应的变动:
template<typename RT, typename T1, typename T2>
class bind
{
typedef RT (*fun_type)(T1, T2);
fun_type f;
arg_list<T1, T2> para_list;
public:
bind(fun_type fun, T1 p1, T2 p2) : f(fun), para_list(p1, p2) {}
bind(fun_type fun, const place_holder<1>& ph1, T2 p2) : f(fun), para_list(ph1, p2) {}
bind(fun_type fun, T1 p1, const place_holder<2>& ph2) : f(fun), para_list(p1, ph2) {}
bind(fun_type fun, const place_holder<1>& ph1, const place_holder<2>& ph2) : f(fun), para_list(ph1, ph2) {}
RT operator()()
{
arg_item<T1, false> *arg1 = dynamic_cast<arg_item<T1, false>*>(para_list.get(1));
arg_item<T2, false> *arg2 = dynamic_cast<arg_item<T2, false>*>(para_list.get(2));
return f(arg1->value(), arg2->value());
}
RT operator()(T1 r1)
{
arg_item<T2, false> *arg2 = dynamic_cast<arg_item<T2, false>*>(para_list.get(2));
return f(r1, arg2->value());
}
RT operator()(T2 r2)
{
arg_item<T1, false> *arg1 = dynamic_cast<arg_item<T1, false>*>(para_list.get(1));
return f(arg1->value(), r2);
}
RT operator()(T1 r1, T2 r2)
{
return f(r1, r2);
}
};
现在如果你将上面测试代码中使用 bind<int, int, bool> add_fun(test, _1, true),而在调用时使用 add_fun(true)时程序就将直接崩溃,因为在对 operator()(T2 r2) 的调用中,取出第一个参数时有一个基类指针到子类指针的转换,转换的目标类型是 arg_item<int, false>,也就是如果要按这种方式调用的话,第一个参数在绑定时就不应该是占位符,而是用真实值进行的绑定,但现在第一个参数由于绑定时使用了占位符,所以实际上它的类型是 arg_item<int, true>,所以这个转换失败,指针 arg1 成为空指针,对它进行解引用调用 value() 成员必定导致程序崩溃。虽然这只有在运行时才会出问题,但总比运行时都能运行要好得多。至于如何让其通不过编译,限于篇幅关系,留再后续文章进行讨论。
临了强调一下,本文中的实现是有缺陷的,比如原始函数的两个参数类型相同,则 bind 中只接受一个参数的 operator() 就有两个版本,所以如果要支持这种情况,需要对 bind 提供一个特化版本,里面只提供一个只接受一个参数的 operator() 函数,另外,本实现也不支持原始函数为成员函数的情况。
最后,将完整的实现代码和测试代码贴在下面:
bind.h
#ifndef __common_test_bind_h__
#define __common_test_bind_h__
#include <vector>
#include <cassert>
namespace common_test
{
namespace bind
{
template<int index>
class place_holder
{
public:
static const int place_index = index;
};
place_holder<1> _1;
place_holder<2> _2;
class arg_item_base
{
public:
virtual ~arg_item_base() {}
};
template<typename T, bool isholder>
class arg_item;
template<typename T>
class arg_item<T, false> : public arg_item_base
{
T val;
public:
static const bool isholder = false;
arg_item() {}
arg_item(const T& v) : val(v){}
T value() {return val;}
};
template<typename T>
class arg_item<T, true> : public arg_item_base
{
public:
static const bool isholder = true;
};
template<typename T1, typename T2>
class arg_list
{
std::vector<arg_item_base*> arg_container;
arg_item<T1, true> ap1;
arg_item<T1, false> a1;
arg_item<T2, true> ap2;
arg_item<T2, false> a2;
public:
template<int index>
struct arg_index_type_traits;
template<>
struct arg_index_type_traits<1>
{
typedef T1 value_type;
};
template<>
struct arg_index_type_traits<2>
{
typedef T2 value_type;
};
public:
arg_list(T1 p1, T2 p2) : a1(p1), a2 (p2)
{
arg_container.push_back(&a1);
arg_container.push_back(&a2);
}
arg_list(const place_holder<1>&, T2 p2) : a2(p2)
{
arg_container.push_back(&ap1);
arg_container.push_back(&a2);
}
arg_list(T1 p1, const place_holder<2>&) : a1(p1)
{
arg_container.push_back(&a1);
arg_container.push_back(&ap2);
}
arg_list(const place_holder<1>&, const place_holder<2>&)
{
arg_container.push_back(&ap1);
arg_container.push_back(&ap2);
}
~arg_list(){}
public:
inline arg_item_base* get(int index)
{
return arg_container[index - 1];
}
};
template<typename RT, typename T1, typename T2>
class bind
{
typedef RT (*fun_type)(T1, T2);
fun_type f;
arg_list<T1, T2> para_list;
public:
bind(fun_type fun, T1 p1, T2 p2) : f(fun), para_list(p1, p2) {}
bind(fun_type fun, const place_holder<1>& ph1, T2 p2) : f(fun), para_list(ph1, p2) {}
bind(fun_type fun, T1 p1, const place_holder<2>& ph2) : f(fun), para_list(p1, ph2) {}
bind(fun_type fun, const place_holder<1>& ph1, const place_holder<2>& ph2) : f(fun), para_list(ph1, ph2) {}
RT operator()()
{
arg_item<T1, false> *arg1 = dynamic_cast<arg_item<T1, false>*>(para_list.get(1));
arg_item<T2, false> *arg2 = dynamic_cast<arg_item<T2, false>*>(para_list.get(2));
return f(arg1->value(), arg2->value());
}
RT operator()(T1 r1)
{
arg_item<T2, false> *arg2 = dynamic_cast<arg_item<T2, false>*>(para_list.get(2));
return f(r1, arg2->value());
}
RT operator()(T2 r2)
{
arg_item<T1, false> *arg1 = dynamic_cast<arg_item<T1, false>*>(para_list.get(1));
return f(arg1->value(), r2);
}
RT operator()(T1 r1, T2 r2)
{
return f(r1, r2);
}
};
}
}
#endif
main.cpp
#include "bind.h"
#include <iostream>
using namespace common_test::bind;
int test(int n, bool b)
{
return b ? ++n : --n;
}
int main()
{
bind<int, int, bool> add_fun(test, _1, true);
std::cout << "add_fun(10) = " << add_fun(10) << std::endl;
bind<int, int, bool> de_fun(test, _1, false);
std::cout << "de_fun(10) = " << de_fun(10) << std::endl;
bind<int, int, bool> u_fun(test, _1, _2);
std::cout << "u_fun(10, true) = " << u_fun(10, true) << std::endl;
std::cout << "u_fun(10, false) = " << u_fun(10, false) << std::endl;
bind<int, int, bool> ua_fun(test, 10, true);
std::cout << "ua_fun() = " << ua_fun() << std::endl;
bind<int, int, bool> ud_fun(test, 10, false);
std::cout << "ud_fun() = " << ud_fun() << std::endl;
return 0;
}