函数组合,Part II
在SGI STL中的另一个常用的函数组合是 compose1 ,在 Boost.Compose 中是compose_f_gx 。这些函数提供了用一个参数调用两个函数的方法,把最里面的函数返回的结果传递给第一个函数。有时一个例子胜过千言万语,设想你需要对容器中的浮点数元素执行两个算术操作。我们首先把值增加10%,然后再减少10%;这个例子对于少数工作在财政部门的人来说可能是有用的一课。
std::list<double> values; values.push_back(10.0); values.push_back(100.0); values.push_back(1000.0); std::transform( values.begin(), values.end(), values.begin(), boost::bind( std::multiplies<double>(),0.90, boost::bind<double>( std::multiplies<double>(),_1,1.10))); std::copy( values.begin(), values.end(), std::ostream_iterator<double>(std::cout," "));
你怎么知道哪个嵌套的 bind 先被调用呢?你也许已经注意到,总是最里面的 bind 先被求值。这意味着我们可以把同样的代码写得稍微有点不同。
std::transform( values.begin(), values.end(), values.begin(), boost::bind<double>( std::multiplies<double>(), boost::bind<double>( std::multiplies<double>(),_1,1.10),0.90));
这里,我们改变了传给 bind 的参数的顺序,把第一个 bind 的参数加在了表达式的最后。虽然我不建议这样做,但它对于理解参数如何传递给 bind 函数很有帮助。
bind 表达式中的是值语义还是指针语义?
当我们传递某种类型的实例给一个 bind 表达式时,它将被复制,除非我们显式地告诉bind 不要复制它。要看我们怎么做,这可能是至关重要的。为了看一下在我们背后发生了什么事情,我们创建一个 tracer 类,它可以告诉我们它什么时候被缺省构造、被复制构造、被赋值,以及被析构。这样,我们就可以很容易看到用不同的方式使用 bind 会如何影响我们传送的实例。以下是完整的 tracer 类。
class tracer { public: tracer() { std::cout << "tracer::tracer()\n"; } tracer(const tracer& other) { std::cout << "tracer::tracer(const tracer& other)\n"; } tracer& operator=(const tracer& other) { std::cout << "tracer& tracer::operator=(const tracer& other)\n"; return *this; } ~tracer() { std::cout << "tracer::~tracer()\n"; } void print(const std::string& s) const { std::cout << s << '\n'; } };
我们把我们的 tracer 类用于一个普通的 bind 表达式,象下面这样。
tracer t; boost::bind(&tracer::print,t,_1) (std::string("I'm called on a copy of t\n"));
运行这段代码将产生以下输出,可以清楚地看到有很多拷贝产生。
tracer::tracer() tracer::tracer(const tracer& other) tracer::tracer(const tracer& other) tracer::tracer(const tracer& other) tracer::~tracer() tracer::tracer(const tracer& other) tracer::~tracer() tracer::~tracer() I'm called on a copy of t tracer::~tracer() tracer::~tracer() // 译注:原文没有这一行,有误
如果我们使用的对象的拷贝动作代价昂贵,我们也许就不能这样用 bind 了。但是,拷贝还是有优点的。它意味着 bind 表达式以及由它所得到的绑定器不依赖于原始对象(在这里是 t)的生存期,这通常正是想要的。要避免复制,我们必须告诉 bind 我们想传递引用而不是它所假定的传值。我们要用 boost::ref 和 boost::cref (分别用于引用和 const 引用)来做到这一点,它们也是 Boost.Bind 库的一部分。对我们的 tracer 类使用 boost::ref,测试代码现在看起来象这样:
tracer t; boost::bind(&tracer::print,boost::ref(t),_1)( std::string("I'm called directly on t\n"));
Executing the code gives us this:
tracer::tracer() I'm called directly on t tracer::~tracer() // 译注:原文为 tracer::~tracer,有误
这正是我们要的,避免了无谓的复制。bind 表达式使用原始的实例,这意味着没有tracer 对象的拷贝了。当然,它同时也意味着绑定器现在要依赖于 tracer 实例的生存期了。还有一种避免复制的方法;就是通过指针来传递参数而不是通过值来传递。
tracer t; boost::bind(&tracer::print,&t,_1)( std::string("I'm called directly on t\n"));
因此说,bind 总是执行复制。如果你通过值来传递,对象将被复制,这可能对性能有害或者产生不必要的影响。为了避免复制对象,你可以使用 boost::ref/boost::cref 或者使用指针语义。
虚拟函数也可以绑定
到目前为止,我们看到了 bind如何可以用于非成员函数和非虚拟成员函数,但是它也可以用于绑定一个虚拟成员函数。通过 Boost.Bind, 你可以象使用非虚拟函数一样使用虚拟函数,即把它绑定到最先声明该成员函数为虚拟的基类的那个虚拟函数上。这个绑定器就可以用于所有的派生类。如果你绑定到其它派生类,你就限制了可以使用这个绑定器的类[5]。考虑以下两个类 base 和 derived :
[5] 这与声明一个类指针来调用虚拟函数没有什么不同。指针指向的派生类越靠近底层,则越少的类可以绑定到指针。
class base { public: virtual void print() const { std::cout << "I am base.\n"; } virtual ~base() {} }; class derived : public base { public: void print() const { std::cout << "I am derived.\n"; } };
我们可以用这两个类对绑定到虚拟函数进行测试,如下:
derived d; base b; boost::bind(&base::print,_1)(b); boost::bind(&base::print,_1)(d);
运行这段代码可以清楚地看到结果正是我们所希望的。
I am base. I am derived.
对于可以支持虚拟函数,你应该不会惊讶,现在我们已经示范了它和其它函数一样运行。有一个相关的注意事项,如果你 bind 了一个成员函数而后来它被一个派生类重新定义了,或者一个虚拟函数在基类中是公有的而在派生类中变成了私有的,那么会发生什么呢?还可以正常工作吗?如果可以,你希望是哪一种行为呢?是的,不管你是否使用 Boost.Bind,行为都不会有变化。因面,如果你 bind到一个在其它类中被重新定义的函数,即它不是虚拟的并且派生类有一个相同特征的成员函数,那么基类中的版本将被调用。如果函数被隐藏,绑定器依然会被执行,因为它显式地访问类型中的函数,这样即使是被隐藏的成员函数也可以使用。最后,如果虚拟函数在基类中声明为公有的,但在派生类中变成了私有的,那么对一个派生类实例调用该函数将会成功,因为访问是通过一个基类实例产生的,而基类的成员函数是公有的。当然,这种情况显示出设计的确是有问题的。
绑定到成员变量
很多时候你需要 bind 数据成员而不是成员函数。例如,使用 std::map 或 std::multimap时,元素的类型是 std::pair<key const,data>, 但你想使用的信息通常不是 key, 而是 data. 假设你想把一个 map 中的每个元素传递给一个函数,它接受单个 data 类型的参数。你需要创建一个绑定器,它把每个元素(类型为 std::pair)的 second 成员传给绑定的函数。以下代码举例说明如何实现:
void print_string(const std::string& s) { std::cout << s << '\n'; } std::map<int,std::string> my_map; my_map[0]="Boost"; my_map[1]="Bind"; std::for_each( my_map.begin(), my_map.end(), boost::bind(&print_string, boost::bind( &std::map<int,std::string>::value_type::second,_1)));
你可以 bind 到一个成员变量,就象你可以绑定一个成员函数或普通函数一样。要注意的是,要使得代码更易读(和写),使用短的、方便的名字是个好主意。在前例中,对std::map 使用一个 typedef 有助于提高可读性。
typedef std::map<int,std::string> map_type; boost::bind(&map_type::value_type::second,_1)));
虽然需要 bind 到成员变量的时候没有象成员函数那么多,但是可以这样做还是很方便的。SGI STL (及其派生的库)的用户可能很熟悉 select1st 和 select2nd 函数。它们用于选出 std::pair 的 first 或 second 成员,与我们在这个例子中所做的一样。注意,bind可以用于任意类型和任意名字。
绑定还是不绑定
Boost.Bind 库带来了很大的灵活性,但是也给程序员带来了挑战,因为有些时候本应该使用独立的函数对象的,但也会让人倾向于使用绑定器。许多工作可以也应该利用 Bind 来完成,但过度使用也是一种错误,应该在代码开始变得难以阅读、理解和维护的地方画一条分界线。不幸的是,分界线的位置是由分享(阅读、维护和扩展)代码的程序员所决定的,他们的经验决定了什么是可以接受的,什么不是。使用专门的函数对象的好处是,它们通常是无需加以说明的,而使用绑定器来提供同样清楚的信息则是一项我们必须坚持克服的挑战。例如,如果你需要创建一个你都很难弄明白的嵌套 bind ,有可能就是你已经过度使用了。让我们用代码来解释一下。
#include <iostream> #include <string> #include <map> #include <vector> #include <algorithm> #include "boost/bind.hpp" void print(std::ostream* os,int i) { (*os) << i << '\n'; } int main() { std::map<std::string,std::vector<int> > m; m["Strange?"].push_back(1); m["Strange?"].push_back(2); m["Strange?"].push_back(3); m["Weird?"].push_back(4); m["Weird?"].push_back(5); std::for_each(m.begin(),m.end(), boost::bind(&print,&std::cout, boost::bind(&std::vector<int>::size, boost::bind( &std::map<std::string, std::vector<int> >::value_type::second,_1)))); }
上面这段代码实际上做了什么?有的人可以流畅地阅读这段代码[6],但对于我们多数人来说,需要一些时间才能搞清楚它是干嘛的。是的,绑定器对 pair (即std::map<std::string,std::vector<int> >::value_type)的成员 second 调用成员函数size 。这种情况下,简单的问题被绑定器弄得复杂了,创建一个小的函数对象来取代这个让人难以理解的复杂绑定器是更好的选择。一个可以完成相同工作的简单函数对象如下:
[6] 你好,Peter Dimov.
class print_size { std::ostream& os_; typedef std::map<std::string,std::vector<int> > map_type; public: print_size(std::ostream& os):os_(os) {} void operator()( const map_type::value_type& x) const { os_ << x.second.size() << '\n'; } };
这种时候使用函数对象的最大好处就是,名字是无需加以说明的。
std::for_each(m.begin(),m.end(),print_size(std::cout));
我们把这些(函数对象以及实际调用的所有代码)和前面使用绑定器的版本作一下比较。
std::for_each(m.begin(),m.end(), boost::bind(&print,&std::cout, boost::bind(&std::vector<int>::size, boost::bind( &std::map<std::string, std::vector<int> >::value_type::second,_1))));
或者,如果我们负点责任,为 vector 和 map 分别创建一个简洁的 typedef :
std::for_each(m.begin(),m.end(), boost::bind(&print,&std::cout, boost::bind(&vec_type::size, boost::bind(&map_type::value_type::second,_1))));
这样可以容易点分析,但它还是有点长。
虽然使用 bind 版本是有一些好理由,但我想观点是很清楚的,绑定器不是非用不可的工具,使用时应该负责任,要让它们物有所值。这一点在使用标准库的容器和算法时非常、非常普遍。当事情变得太过复杂时,就回到老风格的方法上。
让绑定器把握状态
创建一个象 print_size 那样的函数对象时,有几个选项可用。我们在上一节中创建的那个版本中,保存了一个到 std::ostream 的引用,并使用这个 ostream 来打印map_type::value_type 参数的成员 second 的 size 函数的返回值。以下是原来的print_size :
class print_size { std::ostream& os_; typedef std::map<std::string,std::vector<int> > map_type; public: print_size(std::ostream& os):os_(os) {} void operator()( const map_type::value_type& x) const { os_ << x.second.size() << '\n'; } };
要重点关注的一点是,这个类是有状态的,状态就在于那个保存的 std::ostream. 我们可以通过向调用操作符增加一个 ostream 参数来去掉这个状态。这意味着这个函数对象将变为无状态的。
class print_size { typedef std::map<std::string,std::vector<int> > map_type; public: typedef void result_type; result_type operator()(std::ostream& os, const map_type::value_type& x) const { os << x.second.size() << '\n'; } };
注意,这个版本的 print_size 可以很好地用于 bind, 因为它增加了一个 result_type typedef. 这样用户在使用 bind 时就不需要显式声明函数对象的返回类型。在这个新版本的 print_size 里,用户需要传递一个 ostream 参数来调用它。这在使用绑定器时是很容易的。用这个新的 print_size 重写前节中的例子,我们可以得到:
#include <iostream> #include <string> #include <map> #include <vector> #include <algorithm> #include "boost/bind.hpp" // 省略 print_size 的定义 int main() { typedef std::map<std::string,std::vector<int> > map_type; map_type m; m["Strange?"].push_back(1); m["Strange?"].push_back(2); m["Strange?"].push_back(3); m["Weird?"].push_back(4); m["Weird?"].push_back(5); std::for_each(m.begin(),m.end(), boost::bind(print_size(),boost::ref(std::cout),_1)); }
细心的读者可能觉得为什么 print_size 不是一个普通函数,毕竟它已经不带有任何状态了。事实上,它可以是普通函数。
void print_size(std::ostream& os, const std::map<std::string,std::vector<int> >::value_type& x) { os << x.second.size() << '\n'; }
还有更多的泛化工作可以做。我们当前版本的 print_size 要求其调用操作符的第二个参数是一个 const std::map<std::string,std::vector<int> > 引用,这不够通用。我们可以做得更好一些,让调用操作符对这个类型进行泛化。这样,print_size 就可以使用任意类型的参数,只要该参数含有名为 second 的公有成员,并且该成员有一个成员函数 size. 以下是改进后的版本:
class print_size { public: typedef void result_type; template <typename Pair> result_type operator() (std::ostream& os,const Pair& x) const { os << x.second.size() << '\n'; } };
这个版本的用法与前一个是一样的,但它更为灵活。在创建可用于 bind 表达式的函数对象时,这种泛化更为重要。因为这样的函数对象可用的情形将显著增加,多数潜在的泛化都是值得做的。既然如此,我们还可以进一步放松对使用 print_size 的类型的要求。当前版本的 print_size 要求调用操作符的第二个参数是一个类似于 pair 的对象,即一个含有名为 second 的成员的对象。如果我们决定只要求这个参数含有成员函数 size, 这个函数对象就真的与它的名字相符了。
class print_size { public: typedef void result_type; template <typename T> void operator() (std::ostream& os,const T& x) const { os << x.size() << '\n'; } };
当然,尽管 print_size 现在是与它的名字相符了,但是我们也要求用户要做的更多了。象对于我们前面的例子,就需要手工绑定一个 map_type::value_type::second.
std::for_each(m.begin(),m.end(), boost::bind(print_size(),boost::ref(std::cout), boost::bind(&map_type::value_type::second,_1)));
在使用 bind 时,通常都需要这样的折衷,泛化只能到此为止,不要损害到可用性。如果我们走到极端,甚至去掉对成员函数 size 的要求,那么我们就转了一圈,回到了我们开始的地方,又回到那个对多数程序员而言都过于复杂的 bind 表达式了。
std::for_each(m.begin(),m.end(), boost::bind(&print[7],&std::cout, boost::bind(&vec_type::size, boost::bind(&map_type::value_type::second,_1))));
[7] print 函数显然也是需要的,没有
lambda 工具。
关于 Boost.Bind 和 Boost.Function
虽然本章中讨论的内容应该没有遗漏了,但是对于 Boost.Bind 和另一个库,Boost.Function,之间的配合还是值得一提,它可以提供更多的功能。我们将在 "Library 11:Function 11" 看到,不过我还是想给你一些提示。正如我们所看到的,没有一个明显的方法来保存我们的绑定器以备后用,我们只知道它们是带有某些(未知)的特征的兼容函数对象。但是,如果使用 Boost.Function, 保存函数用于以后的调用正是那个库要做的,并且它兼容于 Boost.Bind, 可以把绑定器赋值给函数,保存它们并用于以后的调用。这是一个非常有用的概念,它可以用于适配并提高了松耦合。