【Boost】boost库中function和bind一起使用的技巧(二)

与 Boost.Function 一起使用 Boost.Bind  

当我们把 Boost.Function 与某个支持参数绑定的库结合起来使用时,事情变得更为有趣。Boost.Bind 为普通函数、成员函数以及成员变量提供参数绑定。这非常适合于 Boost.Function, 我们常常需要这类绑定,由于我们使用的类本身并不是函数对象。那么,我们用 Boost.Bind 把它们转变为函数对象,然后我们可以用 Boost.Function 来保存它们并稍后调用。在将图形用户界面(GUIs)与如何响应用户的操作进行分离时,几乎总是要使用某种回调方法。如果这种回调机制是基于函数指针的,就很难避免对可以使用回调的类型的某些限制,也就增加了界面表现与业务逻辑之间的耦合风险。通过使用 Boost.Function,我们可以避免这些事情,并且当与某个支持参数绑定的库结合使用时,我们可以轻而易举地把上下文提供给调用的函数。这是本库最常见的用途之一,把业务逻辑即从表示层分离出来。

以下例子包含一个艺术级的磁带录音机,定义如下:

 
 
  1. class tape_recorder {  
  2. public:  
  3.   void play() {  
  4.     std::cout << "Since my baby left me.../n";  
  5.   }  
  6.   
  7.   void stop() {  
  8.     std::cout << "OK, taking a break/n";  
  9.   }  
  10.   
  11.   void forward() {  
  12.     std::cout << "whizzz/n";  
  13.   }  
  14.   
  15.   void rewind() {  
  16.     std::cout << "zzzihw/n";  
  17.   }  
  18.   
  19.   void record(const std::string& sound) {  
  20.     std::cout << "Recorded: " << sound << '/n';  
  21.   }  
  22. };  

这个磁带录音机可以从一个GUI进行控制,或者也可能从一个脚本客户端进行控制,或者从别的源进行控制,这意味着我们不想把这些函数的执行与它们的实现耦合起来。建立这种分离的一个常用的方法是,用专门的对象负责执行命令,而让客户对命令如何执行毫无所知。这也被称为命令模式(Command pattern),并且在它非常有用。这种模式的特定实现中的一个问题是,需要为每个命令创建单独的类。以下片断示范了它看起来是个什么样子:

 
 
  1. class command_base {  
  2. public:  
  3.   virtual bool enabled() const=0;  
  4.   virtual void execute()=0;  
  5.   
  6.   virtual ~command_base() {}  
  7. };  
  8.   
  9. class play_command : public command_base {  
  10.   tape_recorder* p_;  
  11. public:  
  12.   play_command(tape_recorder* p):p_(p) {}  
  13.   
  14.   bool enabled() const {  
  15.     return true;  
  16.   }  
  17.   
  18.   void execute() {  
  19.     p_->play();  
  20.   }  
  21. };  
  22.   
  23. class stop_command : public command_base {  
  24.   tape_recorder* p_;  
  25. public:  
  26.   stop_command(tape_recorder* p):p_(p) {}  
  27.   
  28.   bool enabled() const {  
  29.     return true;  
  30.   }  
  31.   
  32.   void execute() {  
  33.     p_->stop();  
  34.   }  
  35. };  

这并不是一个非常吸引的方案,因为它使得代码膨胀,有许多简单的命令类,而它们只是简单地负责调用一个对象的单个成员函数。有时候,这是必需的,因为这些命令可能需要实现业务逻辑和调用函数,但通常它只是由于我们所使用的工具有所限制而已。这些命令类可以这样使用:

 
 
  1. int main() {  
  2.   tape_recorder tr;  
  3.   
  4.   // 使用命令模式  
  5.   command_base* pPlay=new play_command(&tr);  
  6.   command_base* pStop=new stop_command(&tr);  
  7.   
  8.   // 在按下某个按钮时调用  
  9.   pPlay->execute();  
  10.   pStop->execute();  
  11.   
  12.   delete pPlay;  
  13.   delete pStop;  
  14. }  

现在,不用再创建额外的具体的命令类,如果我们实现的命令都是调用一个返回 void 且没有参数(先暂时忽略函数 record, 它带有一个参数)的成员函数的话,我们可以来点泛化。不用再创建一组具体的命令,我们可以在类中保存一个指向正确成员函数的指针。这是迈向正确方向[6]的一大步,就象这样:

[6] 虽然损失了一点效率。

 
 
  1. class tape_recorder_command : public command_base {  
  2.   void (tape_recorder::*func_)();   
  3.   tape_recorder* p_;  
  4. public:  
  5.   
  6.   tape_recorder_command(  
  7.     tape_recorder* p,  
  8.     void (tape_recorder::*func)()) : p_(p),func_(func) {}  
  9.    
  10.   bool enabled() const {  
  11.     return true;  
  12.   }  
  13.   
  14.   void execute() {  
  15.     (p_->*func_)();  
  16.   }  
  17. };  

这个命令模式的实现要好多了,因为它不需要我们再创建一组完成相同事情的独立的类。这里的不同在于我们保存了一个 tape_recorder 成员函数指针在 func_ 中,它要在构造函数中提供。命令的执行部分可能并不是你要展现给你的朋友看的东西,因为成员指针操作符对于一些人来说可能还不太熟悉。但是,这可以被看为一个低层的实现细节,所以还算好。有了这个类,我们可以进行泛化处理,不再需要实现单独的命令类。

 
 
  1. int main() {  
  2.   tape_recorder tr;  
  3.   
  4.   // 使用改进的命令模式  
  5.   command_base* pPlay=  
  6.     new tape_recorder_command(&tr,&tape_recorder::play);  
  7.   command_base* pStop=  
  8.     new tape_recorder_command(&tr,&tape_recorder::stop);  
  9.   
  10.   // 从一个GUI或一个脚本客户端进行调用  
  11.   pPlay->execute();  
  12.   pStop->execute();  
  13.   
  14.   delete pPlay;  
  15.   delete pStop;  
  16. }  

你可能还没有理解,我们已经在开始实现一个简单的 boost::function 版本,它已经可以做到我们想要的。不要重复发明轮子,让我们重点关注手边的工作:分离调用与实现。以下是一个全新实现的 command 类,它更容易编写、维护以及理解。

 
 
  1. class command {  
  2.   boost::function<void()> f_;  
  3. public:  
  4.   command() {}  
  5.   command(boost::function<void()> f):f_(f) {}  
  6.   
  7.   void execute() {  
  8.     if (f_) {  
  9.       f_();  
  10.     }  
  11.   }  
  12.   
  13.   template <typename Func> void set_function(Func f) {  
  14.     f_=f;  
  15.   }  
  16.   
  17.   bool enabled() const {  
  18.     return f_;  
  19.   }  
  20. };  

通过使用 Boost.Function,我们可以立即从同时兼容函数和函数对象——包括由绑定器生成的函数对象——的灵活性之中获益。这个 command 类把函数保存在一个返回 void 且不接受参数的 boost::function 中。为了让这个类更加灵活,我们提供了在运行期修改函数对象的方法,使用一个泛型的成员函数,set_function.

 
 
  1. template <typename Func> void set_function(Func f) {  
  2.   f_=f;  
  3. }  

通过使用泛型方法,任何函数、函数对象,或者绑定器都兼容于我们的 command 类。我们也可以选择把 boost:: function 作为参数,并使用 function 的转型构造函数来达到同样的效果。这个 command 类非常通用,我们可以把它用于我们的 tape_recorder 类或者别的地方。与前面的使用一个基类与多个具体派生类(在那里我们使用指针来实现多态的行为)的方法相比,还有一个额外的优点就是,它更容易管理生存期问题,我们不再需要删除命令对象,它们可以按值传递和保存。我们在布尔上下文中使用 function f_ 来测试命令是否可用。如果函数不包含一个目标,即一个函数或函数对象,它将返回 false, 这意味着我们不能调用它。这个测试在 execute 的实现中进行。以下是使用我们这个新类的一个例子:

 
 
  1. int main() {  
  2.   tape_recorder tr;  
  3.   
  4.   command play(boost::bind(&tape_recorder::play,&tr));  
  5.   command stop(boost::bind(&tape_recorder::stop,&tr));  
  6.   command forward(boost::bind(&tape_recorder::stop,&tr));  
  7.   command rewind(boost::bind(&tape_recorder::rewind,&tr));  
  8.   command record;  
  9.   
  10.   // 从某些GUI控制中调用...  
  11.   if (play.enabled()) {  
  12.     play.execute();  
  13.   }  
  14.   
  15.   // 从某些脚本客户端调用...  
  16.   stop.execute();  
  17.   
  18.   // Some inspired songwriter has passed some lyrics  
  19.   std::string s="What a beautiful morning...";  
  20.   record.set_function(  
  21.     boost::bind(&tape_recorder::record,&tr,s));  
  22.   record.execute();  
  23. }  

为了创建一个具体的命令,我们使用 Boost.Bind 来创建函数对象,当通过这些对象的调用操作符进行调用时,就会调用正确的 tape_recorder 成员函数。这些函数对象是自完备的;它们无参函数对象,即它们可以直接调用,无须传入参数,这正是 boost::function<void()> 所表示的。换言之,以下代码片断创建了一个函数对象,它在配置好的 tape_recorder 实例上调用成员函数 play 。

 
 
  1. boost::bind(&tape_recorder::play,&tr)  

通常,我们不能保存 bind 所返回的函数对象,但由于 Boost.Function 兼容于任何函数对象,所以它可以。

 
 
  1. boost::function<void()> f(boost::bind(&tape_recorder::play,&tr));  

注意,这个类也支持调用 record, 它带有一个类型为 const std::string& 的参数,这是由于成员函数set_function. 因为这个函数对象必须是无参的,所以我们需要绑定上下文以便 record 仍旧能够获得它的参数。当然,这是绑定器的工作。因而,在调用 record 之前,我们创建一个包含被录音的字符串的函数对象。

 
 
  1. std::string s="What a beautiful morning...";  
  2. record.set_function(  
  3.   boost::bind(&tape_recorder::record,&tr,s));  

执行这个保存在 record 的函数对象,将在 tape_recorder 实例 tr 上执行 tape_recorder::record,并传入字符串。有了 Boost.Function 和 Boost.Bind, 就可以实现解耦,让调用代码对于被调用代码一无所知。以这种方式结合使用这两个库非常有用。你已经在这个 command 类中看到了,现在我们该清理一下了。由于 Boost.Function 的杰出功能,你所需的只是以下代码:

 
 
  1. typedef boost::function<void()> command;  
与 Boost.Function 一起使用 Boost.Lambda

与 Boost.Function 兼容于由 Boost.Bind 创建的函数对象一样,它也支持由 Boost.Lambda 创建的函数对象。你用 Lambda 库创建的任何函数对象都兼容于相应的 boost::function. 我们在前一节已经讨论了基于绑定的一些内容,使用 Boost.Lambda 的主要不同之处是它能做得更多。我们可以轻易地创建一些小的、无名的函数,并把它们保存在 boost::function 实例中以用于后续的调用。我们已经在前一章中讨论了 lambda 表达式,在那一章的所有例子中所创建的函数对象都可以保存在一个 function 实例中。function 与创建函数对象的库的结合使用会非常强大。

代价的考虑

有一句谚语说,世界上没有免费的午餐,对于 Boost.Function 来说也是如此。与使用函数指针相比,使用 Boost.Function 也有一些缺点,特别是对象大小的增加。显然,一个函数指针只占用一个函数指针的空间大小(这当然了!),而一个 boost::function实例占的空间有三倍大。如果需要大量的回调函数,这可能会成为一个问题。函数指针在调用时的效率也稍高一些,因为函数指针是被直接调用的,而 Boost.Function 可能需要使用两次函数指针的调用。最后,可能在某些需要与C库保持后向兼容的情形下,只能使用函数指针。

虽然 Boost.Function 可能存在这些缺点,但是通常它们都不是什么实际问题。额外增加的大小非常小,而且(可能存在的)额外的函数指针调用所带来的代价与真正执行目标函数所花费的时间相比通常都是非常小的。要求使用函数而不能使用 Boost.Function 的情形非常罕见。使用这个库所带来的巨大优点及灵活性显然超出这些代价。

幕后的细节

至少了解一下这个库如何工作的基础知识是非常值得的。我们来看一下保存并调用一个函数指针、一个成员函数指针和一个函数对象这三种情形。这三种情形是不同的。要真正看到 Boost.Function 如何工作,只有看源代码——不过我们的做法有些不同,我们试着搞清楚这些不同的版本究竟在处理方法上有些什么不同。我们也有一个不同要求的类,即当调用一个成员函数时,必须传递一个实例的指针给 function1 (这是我们的类的名字)的构造函数。function1 支持只有一个参数的函数。与 Boost.Function 相比一个较为宽松的投条件是,即使是对于成员函数,也只需要提供返回类型和参数类型。这个要求的直接结果就是,构造函数必须被传入一个类的实例用于成员函数的调用(类型可以自动推断)。

我们将要采用的方法是,创建一个泛型基类,它声明了一个虚拟的调用操作符函数;然后,从这个基类派生三个类,分别支持三种不同形式的函数调用。这些类负责所有的工作,而另一个类,function1, 依据其构造函数的参数来决定实例化哪一个具体类。以下是调用器的基类,invoker_base.

 
 
  1. template <typename R, typename Arg> class invoker_base {  
  2. public:  
  3.   virtual R operator()(Arg arg)=0;  
  4. };  

接着,我们开始定义 function_ptr_invoker, 它是一个具体调用器,公有派生自 invoker_base. 它的目的是调用普通函数。这个类也接受两个类型,即返回类型和参数类型,它们被用于构造函数,构造函数接受一个函数指针作为参数。

 
 
  1. template <typename R, typename Arg> class function_ptr_invoker   
  2.   : public invoker_base<R,Arg> {  
  3.   R (*func_)(Arg);  
  4. public:  
  5.   function_ptr_invoker(R (*func)(Arg)):func_(func) {}  
  6.   
  7.   R operator()(Arg arg) {  
  8.     return (func_)(arg);  
  9.   }  
  10. };  

这个类模板可用于调用任意一个接受一个参数的普通函数。调用操作符简单地以给定的参数调用保存在 func_ 中的函数。请注意(的确有些奇怪)声明一个保存函数指针的变量的那行代码。

 
 
  1. R (*func_)(Arg);  

你也可以用一个 typedef 来让它好读一些。

 
 
  1. typedef R (*FunctionT)(Arg);  
  2. FunctionT func_;  

接着,我们需要一个可以处理成员函数调用的类模板。记住,它要求在构造时给出一个类实例的指针,这一点与 Boost.Function 的做法不一样。这样可以节省我们的打字,因为是编译器而不是程序员来推导这个类。

 
 
  1. template <typename R, typename Arg, typename T>   
  2. class member_ptr_invoker :   
  3.   public invoker_base<R,Arg> {  
  4.   R (T::*func_)(Arg);  
  5.   T* t_;  
  6. public:  
  7.   member_ptr_invoker(R (T::*func)(Arg),T* t)  
  8.     :func_(func),t_(t) {}  
  9.   
  10.   R operator()(Arg arg) {  
  11.     return (t_->*func_)(arg);  
  12.   }  
  13. };  

这个类模板与普通函数指针的那个版本很相似。它与前一个版本的不同在于,构造函数保存了一个成员函数指针与一个对象指针,而调用操作符则在该对象(t_)上调用该成员函数(func_)。

最后,我们需要一个兼容函数对象的版本。这是所有实现中最容易的一个,至少在我们的方法中是这样。通过使用单个模板参数,我们只表明类型 T 必须是一个真正的函数对象,因为我们想要调用它。说得够多了。

 
 
  1. template <typename R, typename Arg, typename T>   
  2. class function_object_invoker :   
  3.   public invoker_base<R,Arg> {  
  4.   T t_;  
  5. public:  
  6.   function_object_invoker(T t):t_(t) {}  
  7.   
  8.   R operator()(Arg arg) {  
  9.     return t_(arg);  
  10.   }  
  11. };  

现在我们已经有了这些适用的积木,剩下来的就是把它们放在一起组成我们的自己的 boost::function, 即function1 类。我们想要一种办法来发现要实例化哪一个调用器。然后我们可以把它存入一个 invoker_base 指针。这里的窃门就是,提供一些构造函数,它们有能力去检查对于给出的参数,哪种调用器是正确的。这仅仅是重载而已,用了一点点手法,包括泛化两个构造函数。

 
 
  1. template <typename R, typename Arg> class function1 {  
  2.   invoker_base<R,Arg>* invoker_;  
  3. public:  
  4.   function1(R (*func)(Arg)) :   
  5.   invoker_(new function_ptr_invoker<R,Arg>(func)) {}  
  6.    
  7.   template <typename T> function1(R (T::*func)(Arg),T* p) :   
  8.     invoker_(new member_ptr_invoker<R,Arg,T>(func,p)) {}  
  9.   
  10.   template <typename T> function1(T t) :   
  11.     invoker_(new function_object_invoker<R,Arg,T>(t)) {}  
  12.   
  13.   R operator()(Arg arg) {  
  14.     return (*invoker_)(arg);  
  15.   }  
  16.   
  17.   ~function1() {  
  18.     delete invoker_;  
  19.   }  
  20. };  

如你所见,这里面最难的部分是正确地定义出推导系统以支持函数指针、类成员函数以及函数对象。无论使用何种设计来实现这类功能的库,这都是必须的。最后,给出一些例子来测试我们这个方案。

 
 
  1. bool some_function(const std::string& s) {  
  2.   std::cout << s << " This is really neat/n";  
  3.   return true;  
  4. }  
  5.   
  6. class some_class {  
  7. public:  
  8.   bool some_function(const std::string& s) {  
  9.     std::cout << s << " This is also quite nice/n";  
  10.     return true;  
  11.   }  
  12. };  
  13.   
  14. class some_function_object {  
  15. public:  
  16.   bool operator()(const std::string& s) {  
  17.     std::cout << s <<   
  18.       " This should work, too, in a flexible solution/n";  
  19.     return true;  
  20.   }  
  21. };  

我们的 function1 类可以接受以下所有函数。

 
 
  1. int main() {  
  2.   function1<bool,const std::string&> f1(&some_function);  
  3.   f1(std::string("Hello"));  
  4.   
  5.   some_class s;  
  6.   function1<bool,const std::string&>   
  7.     f2(&some_class::some_function,&s);  
  8.    
  9.   f2(std::string("Hello"));  
  10.   
  11.   function1<bool,const std::string&>  
  12.     f3(boost::bind(&some_class::some_function,&s,_1));  
  13.    
  14.   f3(std::string("Hello"));  
  15.   
  16.   some_function_object fso;  
  17.   function1<bool,const std::string&>   
  18.     f4(fso);  
  19.   f4(std::string("Hello"));  
  20. }  

它也可以使用象 Boost.Bind 和 Boost.Lambda 这样的 binder 库所返回的函数对象。我们的类与 Boost.Function 中的类相比要简单多了,但是也已经足以看出创建和使用这样一个库的问题以及相关解决方法。知道一点关于一个库是如何实现的事情,对于有效使用这个库是非常有用的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值