C++“准”标准库Boost学习指南(12):Boost.Signals

Boost.Signals

信号和接收系统,基于称为publisher-subscriber 和 observer的模式,它是在一个最小相关性系统中管理事件的重要工具。很少有大型应用软件不采用这种强大设计模式的某种变形,尽管他们有各自的实现方式。Signals提供了一个已验证的、高效的手段,将信号(events/subjects)的发生和这些信号要通知的接收者(subscribers/observers)进行了分离。

Signals 库如何改进你的程序?
  • 函数和函数对象的灵活多点回调
  • 健壮的触发器及事件处理的机制
  • 兼容于函数对象工厂,如 Boost.Bind 和 Boost.Lambda

Boost.Signals 库具体化了信号(signals)和插槽(slots),信号指的是某种可被"抛出"的东西,而插槽是接收该信号的连接者。这是一种著名的设计模式,它还有另外一些名字 Observer, signals/slots, publisher/subscriber, events (和 event targets),这些名字指的都是同一个东西,指的是一些信息源和某些对这些信息的变化感兴趣的实例之间的一对多关系。这种设计模式的使用有多种情况;最常见的是在GUI代码中,用于使特定动作(例如,用户单击了一个按钮)与其它动作(按钮改变它的外观,执行某个商业逻辑)松散连接。信号与插槽在许多场合都很有用,解耦动作的触发条件(信号)和处理它的代码(一个或多个插槽)。它可用于动态改变处理代码的行为,允许同一信号对应多个处理,或者通过一个信号及插槽的类型间的抽象关联来降低类型依赖性。通过使用 Boost.Signals, 可以创建一些信号来接受任意给定的函数特征的插槽,即插槽接受任意类型的参数。这种方法使得该库非常灵活;它适用于任意范围的信号需求。通过对信号源和处理者的解耦,系统无论在物理和逻辑依赖上都变得更为健壮。它可以让信号类型对插槽类型完全一无所知,反之亦然。这对于更高层次的可复用性是很有必要的,它有助于打破依赖性的循环。因此,一个信号与插槽的库不仅仅关系到面向对象的回调,它也关系到使用它的整个系统的健壮性。

Signals 如何适用于标准库?

C++标准库中没有用于回调的工具,而这种工具显然是需要的。Boost.Signals 使用了与标准库相同的态度进行设计,它是标准库工具箱的一个杰出的扩展。


Signals

头文件: "boost/signals.hpp"
通过单个头文件包含了整个库。

"boost/signals/signal.hpp"
包含了 signal 的定义。

"boost/signals/slot.hpp"
包含了 slot 类的定义。

"boost/signals/connection.hpp"
包含了类 connection 和 scoped_connection 的定义。

要使用这个库,可以包含头文件 "boost/signals.hpp",这样可以确保整个库可用,或者可以按照你的需要包含单独的头文件。Boost.Signals 库的核心部分定义在名字空间 boost 中,高级特性则定义在 boost::signals 中。

以下是 signal 的部分内容,其后将对其中最主要的成员函数进行了简要的介绍。如果你需要完整的参考,请见 Signals 的在线文档。

namespace boost {

  template<typename Signature,
  // Function type R(T1, T2, ..., TN)
    typename Combiner = last_value<R>,
    typename Group = int,
    typename GroupCompare = std::less<Group>,
    typename SlotFunction = function<Signature> >
  class signal : public signals::trackable,
                 private noncopyable {
  public:
    signal(const Combiner&=Combiner(),
           const GroupCompare&=GroupCompare());

    ~signal();

    signals::connection connect(const slot_type&);
    signals::connection connect(
      const Group&,
      const slot_type&);

    void disconnect(const Group&);

    std::size_t num_slots() const;

    result_type operator()
      (T1, T2, ..., TN);
  };
}


类型

我们先来看看 signal 的模板参数。除了第一个参数,其它参数都有相应的缺省值,这有助于理解这些参数的基本意思。第一个模板参数是被调用的函数的签名。在这种 signals 的情况下,signal 本身就是被调用的实体。声明这个签名时,使用与普通函数签名相同的语法。例如,一个返回 double 且接受一个类型 int 的参数的函数签名应该象这样:

  1. signal<double(int)>
Combiner 参数表示一个函数对象,它负责逐个对该 signal 所有已连接的插槽(slot)进行调用。它同时也决定如何将组合这些调用返回的结果。缺省的类型是 last_value, 它只是简单地返回最后一个插槽的调用结果

Groups 参数是用于组合所有连接到 signal 的插槽的一种类型。通过连接到不同的插槽组,你可以预设调用插槽的顺序,同时也可以断开插槽组。

GroupCompare 参数决定了如何排序 Groups,缺省值为 std::less<Group>, 通常它都是正确的。如果 Groups 使用了定制的类型,就有可能需要其它的排序方法。
最后,SlotFunction 参数表示插槽函数的类型,缺省值为 boost::function. 我想不出有什么理由改变这个缺省值。这个模板参数用于定义插槽的类型,定义的方法是一个公有的 typedef slot<SlotFunction> slot_type.

成员函数

signal(const Combiner&=Combiner(),
  const GroupCompare&=GroupCompare());
在构造一个 signal 时,可以传入一个 Combiner,它是一个负责在信号到达时调用相应插槽并对返回值进行处理的对象。

~signal();
析构函数在析构时断开所有已连接的插槽。

signals::connection connect(const slot_type& s);
connect 函数把插槽 s 连接到 signal. 函数指针、函数对象、bind 表达式或者 lambda 表达式都可以用作插槽。connect 返回一个 signals::connection, 它是代表被创建的连接的句柄。通过使用这个句柄,插槽可以从 signal 断开,或者你也可以测试该插槽是否还有连接。

signals::connection connect(const Group& g, const slot_type& s);
这个 connect 的重载版本与前一个作用相似,但是它还把插槽 s 连接到组 g. 把一个插槽连接到一个组意味着当一个 signal 产生时,属于较前面的组的插槽会先被调用(即按组的顺序来调用,signal 模板的 GroupCompare 参数定义了组的顺序),而且属于组的所有插槽会在不属于组的插槽之前被调用(可能只有部分插槽是在组中的)。

void disconnect(const Group& g);
断开所有属于组 g 的已连接插槽。

std::size_t num_slots() const;
返回当前连接到 signal 的插槽数量。要测试插槽是否为空,应该调用函数 empty,而不要调用 num_slots 并测试其返回是否为0,因为 empty 的效率更高。

result_type operator()(T1, T2, ..., TN);
signals 使用调用操作符来调用。当信号产生时,必须传递适当的参数给调用操作符,必须符合 signal 的签名(即声明 signal 类型时的第一个模板参数)。参数的类型必须可以隐式转换为信号所需的类型,只有这样调用才可以成功。

Boost.Signals 中还有其它的类型,我们将在本章剩余部分讨论它们。我们还将讨论 signal 类中有用的 typedefs。


Boost.Signals 用法
当你面对需要用多段代码来处理一个事件的情况时,典型的解决方案有:用函数指针进行回调,或者直接对产生事件的子系统与处理事件的子系统之间的依赖性进行编码。这种设计常常会导致循环的依赖性。通过使用 Boost.Signals, 你将获得灵活性和解耦。要开始使用这个库,首先要包含头文件 "boost/signals.hpp".

以下例子示范了 signals 和插槽(slots)的基本特性,包括如何连接它们以及如何产生一个 signal. 注意,插槽指的是由你提供的一个兼容于 signal 的函数签名的函数或函数对象。在以下代码中,我们既创建了一个普通函数,my_first_slot, 也创建了一个函数对象,my_second_slot; 它们两个都将连接到我们创建的一个 signal 上。

#include <iostream>
#include "boost/signals.hpp"

void my_first_slot() {
  std::cout << "void my_first_slot()\n";
}

class my_second_slot {
public:
  void operator()() const {
    std::cout <<
      "void my_second_slot::operator()() const\n";
  }
};

int main() {
  boost::signal<void ()> sig;

  sig.connect(&my_first_slot);
  sig.connect(my_second_slot());

  std::cout << "Emitting a signal...\n";
  sig();
}


我们首先声明一个 signal, 它所需的插槽为返回 void 且不带参数。然后,我们把两个兼容的插槽类型连接到该 signal. 对于第一个插槽,我们用普通函数 my_first_slot 的地址调用 connect。对于另一个插槽,我们缺省构造一个函数对象 my_second_slot 的实例并把它传给 connect。这些连接意味着当我们产生一个 signal (通过调用 sig)时,这两个插槽将被立即调用。

  1. sig();
运行这个程序,输出信息如下:
Emitting a signal...
void my_first_slot()
void my_second_slot::operator()() const

但是,后两行的顺序不一定是这样的,因为属于同一个组的插槽会以不确定的顺序执行。没有办法确定哪一个插槽会先被调用。如果插槽的调用顺序事关紧要,你就必须把它们放入不同的组。

插槽分组

有时候,某些插槽需要在其它插槽之前调用,例如某些插槽会产生一些副作用而别的插槽需要依赖于这些副作用。分组就是支持这种需求的方法。signal 有一个模板参数,名为 Group,其缺省值为 int. Groups 缺省以 std::less<Group> 为排序标准,对于 int 就是 operator< 。换句话说,属于 group 0 的插槽会在 group 1 的插槽之前调用,等等。但是请注意,同一个组中的插槽的调用顺序是不确定的。要严格控制所有插槽的调用顺序,唯一的办法就是把每个插槽都安排到各自的组中。
把插槽指定到一个组的方法是,传递一个 Group 给 signal::connect. 一个已连接插槽不能改变其所属的组;要改变一个插槽所属的
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值