深入理解信号槽机制

深入理解信号槽(一)

这篇文章来自于 A Deeper Look at Signals and Slots,Scott Collins 2005.12.19。需要说明的是,我们这里所说的“信号槽”不仅仅是指 Qt 库里面的信号槽,而是站在一个全局的高度,从系统的角度来理解信号槽。所以在这篇文章中,Qt 信号槽仅仅作为一种实现来介绍,我们还将介绍另外一种信号槽的实现——boost::signal。因此,当你在文章中看到一些信号的名字时,或许仅仅是为了描述方便而杜撰的,实际并没有这个信号。

什么是信号槽(slot:插槽)

这个问题我们可以从两个角度来回答,一个简短一些,另外一个则长些。

让我们先用最简洁的语言来回答这个问题——什么是信号槽?

  • 信号槽是观察者模式的一种实现,或者说是一种升华;
  • 一个信号就是一个能够被观察的事件,或者至少是事件已经发生的一种通知;
  • 一个槽就是一个观察者,通常就是在被观察的对象发生改变的时候——也可以说是信号发出的时候——被调用的函数;
  • 你可以将信号和槽连接起来,形成一种观察者-被观察者的关系;
  • 当事件或者状态发生改变的时候,信号就会被发出;同时,信号发出者有义务调用所有注册的对这个事件(信号)感兴趣的函数(槽)。

信号和槽是多对多的关系。一个信号可以连接多个槽,而一个槽也可以监听多个信号。

信号可以有附加信息。例如,窗口关闭的时候可能发出 windowClosing 信号,而这个信号就可以包含着窗口的句柄,用来表明究竟是哪个窗口发出这个信号;一个滑块在滑动时可能发出一个信号,而这个信号包含滑块的具体位置,或者新的值等等。我们可以把信号槽理解成函数签名信号只能同具有相同签名的槽连接起来。你可以把信号看成是底层事件的一个形象的名字。比如这个 windowClosing 信号,我们就知道这是窗口关闭事件发生时会发出的。

信号槽实际是与语言无关的,有很多方法都可以实现信号槽,不同的实现机制会导致信号槽差别很大。信号槽这一术语最初来自 Trolltech 公司的 Qt 库(现在已经被 Nokia 收购)。1994年,Qt 的第一个版本发布,为我们带来了信号槽的概念。这一概念立刻引起计算机科学界的注意,提出了多种不同的实现。如今,信号槽依然是 Qt 库的核心之一,其他许多库也提供了类似的实现,甚至出现了一些专门提供这一机制的工具库。

简单了解信号槽之后,我们再来从另外一个角度回答这个问题:什么是信号槽?它们从何而来?

前面我们已经了解了信号槽相关的概念。下面我们将从更细致的角度来探讨,信号槽机制是怎样一步步发展的,以及怎样在你自己的代码中使用它们。

程序设计中很重要的一部分是组件交互:系统的一部分需要告诉另一部分去完成一些操作。让我们从一个简单的例子开始:

// C++
class Button
{
public:
    void clicked(); // something that happens: Buttons may be clicked
};
class Page
{
public:
    void reload(); // ...which I might want to do when a Button is clicked
};

换句话说,Page 类知道如何重新载入页面(reload),Button 有一个动作是点击(click)。假设我们有一个函数返回当前页面 currentPage(),那么,当 button 被点击的时候,当前页面应该被重新载入。

// C++ --- making the connection directly
void Button::clicked()
{
    currentPage()->reload(); // Buttons know exactly what to do when clicked
}

这看起来并不很好。因为 Button 这个类名似乎暗示了这是一个可重用的类,但是这个类的点击操作却同 Page 紧紧地耦合在一起了。这使得只要 button 一被点击,必定调用 currentPage() 的 reload() 函数。这根本不能被重用,或许把它改名叫 PageReloadButton 更好一些。

实际上,不得不说,这确实是一种实现方式。如果 Button::click() 这个函数是 virtual 的,那么你完全可以写一个新类去继承这个 Button:

// C++ --- connecting to different actions by specializing
class Button
{
public:
    virtual void clicked() = 0; // Buttons have no idea what to do when clicked
};

class PageReloadButton : public Button
{
public:
    virtual void clicked() {
        currentPage()->reload(); // ...specialize Button to connect it to a specific action
    }
};

好了,现在 Button 可以被重用了。但是这并不是一个很好的解决方案。

引入回调

让我们停下来,回想一下在只有 C 的时代,我们该如何解决这个问题。如果只有 C,就不存在 virtual 这种东西。重用有很多种方式,但是由于没有了类的帮助,我们采用另外的解决方案:函数指针。

/* C --- connecting to different actions via function pointers */
void reloadPage_action( void* ) /* one possible action when a Button is clicked */
{
    reloadPage(currentPage());
}

void loadPage_action( void* url ) /* another possible action when a Button is clicked */
{
    loadPage(currentPage(), (char*)url);
}

struct Button {
    /* ...now I keep a (changeable) pointer to the function to be called */
    void (*actionFunc_)();
    void* actionFuncData_;
};

void buttonClicked( Button* button )
{
    /* call the attached function, whatever it might be */
    if ( button && button->actionFunc_ )
        (*button->actionFunc_)(button->actionFuncData_);
}

这就是通常所说的“回调”。buttonClicked() 函数在编译期并不知道要调用哪一个函数。被调用的函数是在运行期传进来的。这样,我们的 Button 就可以被重用了,因为我们可以在运行时将不同的函数指针传递进来,从而获得不同的点击操作。

增加类型安全

对于 C++ 或者 Java 程序员来说,总是不喜欢这么做。因为这不是类型安全的(注意 url 有一步强制类型转换)。

我们为什么需要类型安全呢?一个对象的类型其实暗示了你将如何使用这个对象。有了明确的对象类型,你就可以让编译器帮助你检查你的代码是不是被正确的使用了,如同你画了一个边界,告诉编译器说,如果有人越界,就要报错。然而,如果没有类型安全,你就丢失了这种优势,编译器也就不能帮助你完成这种维护。这就如同你开车一样。只要你的速度足够,你就可以让你的汽车飞起来,但是,一般来说,这种速度就会提醒你,这太不安全了。同时还会有一些装置,比如雷达之类,也会时时帮你检查这种情况。这就如同编译器帮我们做的那样,是我们出于一种安全使用的范围内。

回过来再看看我们的代码。使用 C 不是类型安全的,但是使用 C++,我们可以把回调的函数指针和数据放在一个类里面,从而获得类型安全的优势。例如:

// re-usable actions, C++ style (callback objects)
class AbstractAction
{
public:
    virtual void execute() = 0; // sub-classes re-implement this to actually do something
};

class Button
{
    // ...now I keep a (changeable) pointer to the action to be executed
    AbstractAction* action_;
};

void Button::clicked()
{
    // execute the attached action, whatever it may be
    if ( action_ )
        action_->execute();
}

class PageReloadAction : public AbstractAction
    // one possible action when a Button is clicked
{
public:
    virtual void execute() {
        currentPage()->reload();
    }
};
class PageLoadAction : public AbstractAction
    // another possible action when a Button is clicked
{
public:
    // ...
    virtual void execute() {
        currentPage()->load(url_);
    }
private:
    std::string url_;
};

好了!我们的 Button 已经可以很方便的重用了,并且也是类型安全的,再也没有了强制类型转换。这种实现已经可以解决系统中遇到的绝大部分问题了。似乎现在的解决方案同前面的类似,都是继承了一个类。只不过现在我们对动作进行了抽象,而之前是对 Button 进行的抽象。这很像前面 C 的实现,我们将不同的动作和 Button 关联起来。现在,我们一步步找到一种比较令人满意的方法。

深入理解信号槽机制(二)

多对多

下一个问题是,我们能够在点击一次重新载入按钮之后做多个操作吗?也就是让信号和槽实现多对多的关系?

实际上,我们只需要利用一个普通的链表,就可以轻松实现这个功能了。比如,如下的实现:

class MultiAction : public AbstractAction
    // ...an action that is composed of zero or more other actions;
    // executing it is really executing each of the sub-actions
{
public:
    // ...
    virtual void execute();
private:
    std::vector<AbstractAction*> actionList_;
    // ...or any reasonable collection machinery
};

void MultiAction::execute()
{
    // call execute() on each action in actionList_
    std::for_each( actionList_.begin(),
                   actionList_.end(),
                   boost::bind(&AbstractAction::execute, _1) );
}

这就是其中的一种实现。不要觉得这种实现看上去没什么水平,实际上我们发现这就是一种相当简洁的方法。同时,不要纠结于我们代码中的 std:: 和 boost:: 这些命名空间,你完全可以用另外的类,强调一下,这只是一种可能的实现。现在,我们的一个动作可以连接多个 button 了,当然,也可以是别的 Action 的使用者。现在,我们有了一个多对多的机制。通过将 AbstractAction* 替换成 boost::shared_ptr,可以解决 AbstractAction 的归属问题,同时保持原有的多对多的关系。

这会有很多的类!

如果你在实际项目中使用上面的机制,很多就会发现,我们必须为每一个 action 定义一个类,这将不可避免地引起类爆炸。至今为止,我们前面所说的所有实现都存在这个问题。不过,我们之后将着重讨论这个问题,现在先不要纠结在这里啦!

特化!特化!

当我们开始工作的时候,我们通过将每一个 button 赋予不同的 action,实现 Button 类的重用。这实际是一种特化。然而,我们的问题是,action 的特化被放在了固定的类层次中,在这里就是这些 Button 类。这意味着,我们的 action 很难被更大规模的重用,因为每一个 action 实际都是与 Button 类绑定的。那么,我们换个思路,能不能将这种特化放到信号与槽连接的时候进行呢?这样,action 和 button 这两者都不必进行特化了。

函数对象

将一个类的函数进行一定程度的封装,这个思想相当有用。实际上,我们的 Action 类的存在,就是将 execute() 这个函数进行封装,其他别无用处。这在 C++ 里面还是比较普遍的,很多时候我们用 c++ 的特性重新封装函数,让类的行为看起来就像函数一样。例如,我们重载 operator() 运算符,就可以让类看起来很像一个函数:

class AbstractAction
{
public:
    virtual void operator()() = 0;
};

// using an action (given AbstractAction& action)
action();

这样,我们的类看起来很像函数。前面代码中的 for_each 也得做相应的改变:

// previously
std::for_each( actionList_.begin(),
               actionList_.end(),
               boost::bind(&AbstractAction::execute, _1) );
// now
std::for_each( actionList_.begin(),
               actionList_.end(),
               boost::bind(&AbstractAction::operator(), _1) );

现在,我们的 Button::clicked() 函数的实现有了更多的选择:

// previously
action_->execute();

// option 1: use the dereferenced pointer like a function
(*action_)();

// option 2: call the function by its new name
action_->operator()();

看起来很麻烦,值得这样做吗?

下面我们来试着解释一下信号和槽的目的。看上去,重写 operator() 运算符有些过分,并不值得我们去这么做。但是,要知道,在某些问题上,你提供的可用的解决方案越多,越有利于我们编写更简洁的代码。通过对一些类进行规范,就像我们要让函数对象看起来更像函数,我们可以让它们在某些环境下更适合重用。在使用模板编程,或者是 Boost.Function,bind 或者是模板元编程的情形下,这一点尤为重要。

这是对无需更多特化建立信号槽连接重要性的部分回答。模板就提供了这样一种机制,让添加了特化参数的代码并不那么难地被特化,正如我们的函数对象那样。而模板的特化对于使用者而言是透明的。

松耦合

现在,让我们回顾一下我们之前的种种做法。

我们执着地寻求一种能够在同一个地方调用不同函数的方法,这实际上是 C++ 内置的功能之一,通过 virtual 关键字,当然,我们也可以使用函数指针实现。当我们需要调用的函数没有一个合适的签名,我们将它包装成一个类。我们已经演示了如何在同一地方调用多个函数,至少我们知道有这么一种方法(但这并不是在编译期完成的)。我们实现了让“信号发送”能够被若干个不同的“槽”监听。

不过,我们的系统的确没有什么非常与众不同的地方。我们来仔细审核一下我们的系统,它真正不同的是:

  • 定义了两个不同的术语:“信号”和“槽”;
  • 在一个调用点(信号)与零个或者多个回调(槽)相连;
  • 连接的焦点从提供者处移开,更多地转向消费者(也就是说,Button 并不需要知道如何做是正确的,而是由回调函数去告知 Button,你需要调用我)。

但是,这样的系统还远达不到松耦合的关系。Button 类并不需要知道 Page 类。松耦合意味着更少的依赖;依赖越少,组件的可重用性也就越高。

当然,肯定需要有组件同时知道 Button 和 Page,从而完成对它们的连接。现在,我们的连接实际是用代码描述的,如果我们不用代码,而用数据描述连接呢?这么一来,我们就有了松耦合的类,从而提高二者的可重用性。

新的连接模式

什么样的连接模式才算是非代码描述呢?假如仅仅只有一种信号槽的签名,例如 void (*signature)(),这并不能实现。使用散列表,将信号的名字映射到匹配的连接函数,将槽的名字映射到匹配的函数指针,这样的一对字符串即可建立一个连接。

然而,这种实现其实包含一些“握手”协议。我们的确希望具有多种信号槽的签名。在信号槽的简短回答中我们提到,信号可以携带附加信息。这要求信号具有参数。我们并没有处理成员函数与非成员函数的不同,这又是一种潜在的函数签名的不同。我们还没有决定,我们是直接将信号连接到槽函数上,还是连接到一个包装器上。如果是包装器,这个包装器需要已经存在呢,还是我们在需要时自动创建呢?虽然底层思想很简单,但是,真正的实现还需要很好的努力才行。似乎通过类名能够创建对象是一种不错的想法,这取决于你的实现方式,有时候甚至取决于你有没有能力做出这种实现。将信号和槽放入散列表需要一种注册机制。一旦有了这么一种系统,前面所说的“有太多类了”的问题就得以解决了。你所需要做的就是维护这个散列表的键值,并且在需要的时候实例化类。

给信号槽添加这样的能力将比我们前面所做的所有工作都困难得多。在由键值进行连接时,多数实现都会选择放弃编译期类型安全检查,以满足信号和槽的兼容。这样的系统代价更高,但是其应用也远远高于自动信号槽连接。这样的系统允许实例化外部的类,比如 Button 以及它的连接。所以,这样的系统有很强大的能力,它能够完成一个类的装配、连接,并最终完成实例化操作,比如直接从资源描述文件中导出的一个对话框。既然它能够凭借名字使函数可用,这就是一种脚本能力。如果你需要上面所说的种种特性,那么,完成这么一套系统绝对是值得的,你的信号槽系统也会从中受益,由数据去完成信号槽的连接。

对于不需要这种能力的实现则会忽略这部分特性。从这点看,这种实现就是“轻量级”的。对于一个需要这些特性的库而言,完整地实现出来就是一个轻量级实现。这也是区别这些实现的方法之一。

深入理解信号槽(三)

信号槽的实现实例—— Qt 和 Boost

Qt 的信号槽和 Boost.Signals 由于有着截然不同的设计目标,因此二者的实现、强度也十分不同。将二者混合在一起使用也不是不可能的,我们将在本系统的最后一部分来讨论这个问题。

使用信号槽

信号槽是伟大的工具,但是如何能更好的使用它们?相比于直接函数调用,有三点值得我们的注意。一个信号槽的调用:

  • 或许会比直接函数调用耗费更多的时间/空间
  • 可能不能使用 inline;
  • 对于代码阅读者来说可能并不友好。

使用信号槽进行解耦,我们获得的最大的好处是,连接两端的对象不需要知道对方的任何信息。Button 同动作的连接是一个很典型的案例。例如如下信息:

class Elevator
{
public:
    enum Direction { DownDirection=-1, NoDirection=0, UpDirection=1 };
    enum State { IdleState, LoadingState, MovingState };
    // ...
// signals:
    void floorChanged( int newFloor );
    void stateChanged( State newState );
    void directionChanged( Direction newDirection );
};

Elevator 类,也就是电梯,不需要知道有多少显示器正在监听它的信号,也不需要知道这些显示器的任何信息。每一层可能有一个屏幕和一组灯,用于显示电梯的当前位置和方向,另外一些远程操控的面板也会显示出同样的信息。电梯并不关心这些东西。当它穿过(或者停在)某一层的时候,它会发出一个 floorChanged(int) 信号。或许,交通信号灯是更合适的一个例子。

你也可以实现一个应用程序,其中每一个函数调用都是通过信号来触发的。这在技术上说是完全没有问题的,然而却是不大可行的,因为信号槽的使用无疑会丧失一部分代码可读性和系统性能。如何在这其中做出平衡,也是你需要考虑的很重要的一点。

Qt 方式

了解 Qt 信号槽最好的莫过于 Qt 的文档。不过,这里我们从一个小例子来了解信号槽的 Qt 方式的使用。

// Qt Signals and Slots
class Button : public QObject
{
    Q_OBJECT
Q_SIGNALS:
    void clicked();
};

class Page : public QObject
{
    Q_OBJECT
public Q_SLOTS:
    void reload();
};

// given pointers to an actual Button and Page:
connect(button, SIGNAL(clicked()), page, SLOT(reload()));

Boost.Signals 方式

了解 Boost.Signals 的最好方式同样是 Boost 的文档。这里,我们还是先从代码的角度了解一下它的使用。

// Boost.Signals
class Button
{
public:
    boost::signal< void() > clicked;
};

class Page
{
public:
    void reload();
};

// given pointers to an actual Button and Page:
button->clicked.connect( boost::bind(&Page::reload, page) );

对比

或许你已经注意到上面的例子中,无论是 Qt 的实现方式还是 Boost 的实现方式,除了必须的 Button 和 Page 两个类之外,都不需要额外的类。两种实现都解决了类爆炸的问题。下面让我们对照着来看一下我们前面的分析。现在我们有:

  • 两个不同的术语以及各自的动作:信号和槽;
  • 在一个地方(信号)可以连接零个或者多个回调函数(槽),同时也是多对多的;
  • 焦点在于连接本身,而不是提供者或者消费者;
  • 不需要手工为了一个连接创建新的类;
  • 连接仍旧是类型安全的。

这五点是信号槽系统的核心,Qt 和 boost 都拥有这些特性。下面则是二者的不同之处:

 Boost.Signals Qt Signals 和 Slots
 一个信号就是一个对象 信号只能是成员函数
发出信号类似于函数调用 发出信号类似于函数调用,Qt 提供了一个 emit 关键字来完成这个操作
信号可以是全局的、局部的或者是成员对象 信号只能是成员函数
 任何能够访问到信号对象的代码都可以发出信号 只有信号的拥有者才能发出信号
 槽是任何可被调用的函数或者函数对象 槽是经过特别设计的成员函数
 可以有返回值,返回值可以在多个槽中使用 没有返回值
 同步的 同步的或者队列的
 非线程安全 线程安全,可以跨线程使用
 当且仅当槽是可追踪的时候,槽被销毁时,连接自动断开 槽被销毁时,连接都会自动断开(因为所有槽都是可追踪的)
 类型安全(编译器检查) 类型安全(运行期检查)
 参数列表必须完全一致 槽可以忽略信号中多余的参数
 信号、槽可以是模板 信号、槽不能是模板
 C++ 直接实现

  通过由 moc 生成的元对象实现(moc 以及元对象系统都是 C++ 直接实现的)

  没有内省机制 可以通过内省发现

 可以通过元对象调用

 连接可以从资源文件中自动推断出

最重要的是,Qt 的信号槽机制已经深深地植入到框架之中,成为不可分割的一部分。它们可以使用 Qt 专门的开发工具,例如 QtCreator,通过拖拽的方式很轻松的创建、删除、修改。它们甚至可以通过动态加载资源文件,由特定命名的对象自动动态生成。这些都是 boost 作为一个通用库所不可能提供的。

深入理解信号槽(四)

将 Qt 的信号槽系统与 Boost.Signals 结合使用

实际上,将 Qt 的信号槽系统与 Boost.Signals 结合在一起使用并非不可能。通过前面的阐述,我们都知道了二者的不同,至于为什么要将这二者结合使用,则是见仁见智的了。这里,我们给出一种结合使用的解决方案,但是并不是说我们暗示应该将它们结合使用。这应该是具体问题具体分析的。

将 Qt 的信号槽系统与 Boost.Signals 结合使用,最大的障碍是,Qt 使用预处理器定义了关键字 signals,slots 以及 emit。这些可以看做是 Qt 对 C++ 语言的扩展。同时,Qt 也提供了另外一种方式,即使用宏来实现这些关键字。为了屏蔽掉这些扩展的关键字,Qt 4.1 的 pro 文件引入了 no_keywords 选项,以便使用标准 C++ 的方式,方便 Qt 与其他 C++ 同时使用。你可以通过打开 no_keywords 选项,来屏蔽掉这些关键字。下面是一个简单的实现:

# TestSignals.pro (platform independent project file, input to qmake)
# showing how to mix Qt Signals and Slots with Boost.Signals
  #
  # Things you will have in your .pro when you try this...
  #
CONFIG += no_keywords
  # so Qt won't #define any non-all-caps `keywords'
INCLUDEPATH += . /usr/local/include/boost-1_33_1/
  # so we can #include <boost/someheader.hpp>
macx:LIBS += /usr/local/lib/libboost_signals-1_33_1.a
  # ...and we need to link with the Boost.Signals library.
  # This is where it lives on my Mac,
  # other platforms would have to add a line here
  #
  # Things specific to my demo
  #
CONFIG -= app_bundle
  # so I will build a command-line tool instead of a Mac OS X app bundle
HEADERS += Sender.h Receiver.h
SOURCES += Receiver.cpp main.cpp

请注意,我们已经在 pro 文件中打开了 no_keywords 选项,那么,类似 signals 这样的关键字已经不起作用了。所以,我们必须将这些关键字修改成相应的宏的版本。例如,我们需要将 signals 改为 Q_SIGNALS,将 slots 改为 Q_SLOTS 等等。请看下面的代码:

// Sender.h

#include <QObject>
#include <string>
#include <boost/signal.hpp>
class Sender : public QObject
{
    Q_OBJECT
Q_SIGNALS: // a Qt signal
    void qtSignal( const std::string& );
    // connect with
    // QObject::connect(sender, SIGNAL(qtSignal(const std::string&)), ...
public: // a Boost signal for the same signature
    boost::signal< void ( const std::string& ) > boostSignal;
    // connect with
    // sender->boostSignal.connect(...
public: // an interface to make Sender emit its signals
    void sendBoostSignal( const std::string& message ) {
        boostSignal(message);
    }

    void sendQtSignal( const std::string& message ) {
        qtSignal(message);
    }
};

现在我们有了一个发送者,下面来看看接收者:

// Receiver.h
#include <QObject>
#include <string>

class Receiver : public QObject
{
    Q_OBJECT
public Q_SLOTS:
    void qtSlot( const std::string& message );
    // a Qt slot is a specially marked member function
    // a Boost slot is any callable signature
};

// Receiver.cpp
#include "Receiver.h"
#include <iostream>

void Receiver::qtSlot( const std::string& message )
{
    std::cout << message << std::endl;
}

下面,我们来测试一下:

// main.cpp
#include <boost/bind.hpp>
#include "Sender.h"
#include "Receiver.h"

int main( int /*argc*/, char* /*argv*/[] )
{
    Sender* sender = new Sender;
    Receiver* receiver = new Receiver;

    // connect the boost style signal
    sender->boostSignal.connect(boost::bind(&Receiver::qtSlot, receiver, _1));
    // connect the qt style signal
    QObject::connect(sender, SIGNAL(qtSignal(const std::string&)),
                     receiver, SLOT(qtSlot(const std::string&)));
    sender->sendBoostSignal("Boost says 'Hello, World!'");
    sender->sendQtSignal("Qt says 'Hello, World!'");
    return 0;
}

这段代码将会有类似下面的输出:

[506]TestSignals$ ./TestSignals
Boost says 'Hello, World!'
Qt says 'Hello, World!'

我们可以看到,这两种实现的不同之处在于,Boost.Signals 的信号,boostSignal,是 public 的,任何对象都可以直接发出这个信号。也就是说,我们可以使用如下的代码:

sender->boostSignal("Boost says 'Hello, World!', directly");

从而绕过我们设置的 sendBoostSignal() 这个触发函数。另外,我们可以看到,boostSignal 完全可以是一个全局对象,这样,任何对象都可以使用这个信号。而对于 Qt 来说,signal 必须是一个成员变量,在这里,只有 Sender 可以使用我们定义的信号。

这个例子虽然简单,然而已经很清楚地为我们展示了,如何通过 Qt 发出信号来获取 Boost 的行为。在这里,我们使用一个公共的 sendQtSignal() 函数发出 Qt 的信号。然而, 为了从 Boost 的信号获取 Qt 的行为,我们需要多做一些工作:隐藏信号,但是需要提供获取连接的函数。这样看上去有些麻烦:

class Sender : public QObject
{
    // just the changes...
private:
    // our new public connect function will be much easier to understand
    // if we simplify some of the type
    typedef boost::signal< void ( const std::string& ) > signal_type;
    typedef signal_type::slot_type slot_type;
    signal_type boostSignal;
    // our signal object is now hidden
public:
    boost::signals::connection
    connectBoostSignal( const slot_type& slot,
                        boost::signals::connect_position pos = boost::signals::at_back ) {
        return boostSignal.connect(slot, pos);
    }
};

应该说,这样的实现相当丑陋。实际上,我们将 Boost 的信号与连接分割开了。我们希望能够有如下的实现:

// WARNING: no such thing as a connect_proxy
class Sender
{
public:
    connect_proxy< boost::signal< void ( const std::string& ) > >
    someSignal() {
        return someSignal_;
        // ...automatically wrapped in the proxy
    }
private:
    boost::signal< void ( const std::string& ) > someSignal_;
};
sender->someSignal().connect(someSlot);

注意,这只是我的希望,并没有做出实现。如果你有兴趣,不妨尝试一下。

总结

前面啰嗦了这么多,现在总结一下。

信号和槽的机制实际上是观察者模式的一种变形。它是面向组件编程的一种很强大的工具。现在,信号槽机制已经成为计算机科学的一种术语,也有很多种不同的实现。

Qt 信号槽是 Qt 整个架构的基础之一,因此它同 Qt 提供的组件、线程、反射机制、脚本、元对象机制以及可视化 IDE 等等紧密地集成在一起。Qt 的信号是对象的成员函数,所以,只有拥有信号的对象才能发出信号。Qt 的组件和连接可以由非代码形式的资源文件给出,并且能够在运行时动态建立这种连接。Qt 的信号槽实现建立在 Qt 元对象机制之上。Qt 元对象机制由 Qt 提供的 moc 工具实现。moc 也就是元对象编译器,它能够将用户指定的具有 Q_OBJECT 宏的类进行一定程度的预处理,给这个增加元对象能力。

Boost.Signals 是具有静态的类型安全检查的,基于模板的信号槽系统的实现。所有的信号都是模板类 boost::signal 的一个特化;所有的槽函数都具有相匹配的可调用的签名。Boost.Signals 是独立的,不需要内省、元对象系统,或者其他外部工具的支持。然而,Boost.Signals 没有从资源文件动态建立连接的能力。

这两种实现都非常漂亮,并且都具有工业强度。将它们结合在一起使用也不是不可能的,Qt 4.1 即提供了这种可能性。

任何基于 Qt GUI 的系统都会自然而然的使用信号槽。你可以从中获取很大的好处。任何大型的系统,如果希望能够降低组件之间的耦合程度,都应该借鉴这种思想。正如其他的机制和技术一样,最重要的是把握一个度。在正确的地方使用信号槽,可以让你的系统更易于理解、更灵活、高度可重用,并且你的工作也会完成得更快。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值