sigslot.h 中文文档

        C++中的信号和槽


sigslot.h 源码及英文文档可以从这里下载https://github.com/EGeeks/sigslot


1 介绍

本文介绍了sigslot库它使用C++实现类型安全,线程安全的信号/插槽机制。 该库完全使用C ++实现, 并且不需要对源代码进行预处理即可使用。 sigslot库主页http://sigslot.sourceforge.net/ 先看看那里本文的最新版本,以及库本身的最新下载。

1.1 sigslot库范例

大多数传统的C++代码最终归结为一个(可能很多)类,它们通过调用彼此的成员函数进行互操作。 允许类以这种方式进行交互操作通常需要类相当详细地了解对方。 例如,一个家庭自动化系统可能包含如下几个类

class Switch
{
public:
    virtual void Clicked() = 0;
};

class Light
{
public:
    void ToggleState();
    void TurnOn();
    void TurnOff();
};

如果我们想将开关“接线”至灯光,以便点击开关来切换灯光的状态,假设我们无法直接修改Switch或Light,我们需要执行以下操作:
class ToggleSwitch : public Switch
{
public:
ToggleSwitch(Light& lp)
{
    m_lp = lp;
}
virtual void Clicked()
{
    m_lp.ToggleState();
}

private:
    Light& m_lp;
};

Light lp1, lp2;
ToggleSwitch tsw1(lp1), tsw2(lp2);

这是足够公平的,但很难。更好的解决方案是使用信号和槽。 信号和槽允许类
不需要过于详细地如何连接在一起。 以下是Switch和Light的一个本地实现:

class Switch
{
public:
    signal0<> Clicked;
};

class Light : public has_slots<>
{
public:
    void ToggleState();
    void TurnOn();
    void TurnOff();
};

Switch sw1, sw2;
Light lp1, lp2;
主要变化是纯虚函数'Clicked()'已经消失,被一个信号取代。
Light类在很大程度上没有改变,只是它继承了'has_slots'。
而不是需要实现像ToggleSwitch这样混乱的派生类,现在可以'开关' 连接指示灯:

sw1.Clicked.connect(&lp1, &Light::ToggleState);
sw2.Clicked.connect(&lp2, &Light::ToggleState);

使用信号与槽后现在变得很清晰, 现在添加两个lights, 各自有自己的触发开关, 加一个全局的全开全关开关

Switch sw3, sw4, all_on, all_off;
Light lp3, lp4;

sw3.Clicked.connect(&lp3, &Light::ToggleState);
sw4.Clicked.connect(&lp4, &Light::ToggleState);
all_on.Clicked.connect(&lp1, &Light::TurnOn());
all_on.Clicked.connect(&lp2, &Light::TurnOn());
all_on.Clicked.connect(&lp3, &Light::TurnOn());
all_on.Clicked.connect(&lp4, &Light::TurnOn());
all_off.Clicked.connect(&lp1, &Light::TurnOff());
all_off.Clicked.connect(&lp2, &Light::TurnOff());
all_off.Clicked.connect(&lp3, &Light::TurnOff());
all_off.Clicked.connect(&lp4, &Light::TurnOff());

1.2 参数类型

信号和槽可以选择使用任意类型的一个或多个参数。 该库C++模板实现,这意味着信号和声明是完全类型检查的。命名约定如下:signal n < type1, type2, ...> ;n 表示参数个数


在下面的例子中,封装窗口的类发送各种信号在窗口被移动,调整大小,打开或关闭:

class Window
{
public:
    enum WindowState { Minimised, Normal, Maximised };
    signal1 StateChanged;
    signal2 MovedTo;
    signal2 Resized;
};
 
class MyControl : public Control, public has_slots<>
{
public:
    void OnStateChanged(WindowState ws);
    void OnMovedTo(int x, int y);
    void OnResize(int x, int y);
};
 
Window w;
MyControl c;
w.StateChanged.connect(&c, &MyControl::OnStateChanged);
w.MovedTo.connect(&c, &MyControl::OnMovedTo);
w.Resized.connect(&c, &MyControl::OnResize);

值得记住的是,只有信号和槽的类型一致才能进行连接被执行 - 使用的名字并不重要。


2 库的使用

2.1 发射信号

当信号被触发时,这通常被称为发射信号。 信号声明:

signal1 ReportError;

可以通过调用其函数操作符来使其发出信号。 在实践中,这看起来调用一个函数

ReportError("Something went wrong", ERR_SOMETHING_WRONG);
或者,您可以通过调用emit()成员函数来获得完全相同的结果:
ReportError.emit("Something went wrong", ERR_SOMETHING_WRONG);


2.2 连接信号

     通过调用信号的connect()成员函数来连接信号。 connect()有两个参数:一个指向目标类的指针和一个指向目标类成员函数的指针。为了实现这一点,所有类型都必须同意,并且目的类也需要继承'has_slots'。
     信号可以连接到任意数量的槽。 当一个信号emit被调用时,所有连接的槽被调用。 这就是为什么插槽始终具有void返回类型, 当信号emit调用,实现返回值是没有意义的。
     当前实现使用STL列表来实现槽连接列表。 意即该槽按与连接顺序相同的顺序调用。 但是可能依靠这个不明智,因为未来调整到sigslot库可能会改变这种行为。


2.3 断开信号

断开信号操作是非常罕见的, 因为离开作用域后自动断开信号但是如果您需要这样做,您可以调用信号的disconnect()成员函数与目标类的指针:

signal1 Bang;
...
Bang.connect(&someobj, &SomeObj::OnBang);
...
Bang(123); // Calls someobj.OnBang()
...
Bang.disconnect(&someobj);
...
Bang(321); // No longer calls someobj.OnBang()


2.4 实现

插槽只是普通的成员函数,具有以下附加条件:
1. 槽必须有返回void
2. 槽必须有0到8个参数(可以是任何类型)。
3. 实现槽的类必须继承has_slots<>。

槽可以通过信号/槽机制调用,也可以直接作为普通成员函数调用。


2.5 完全断开信号

要从当前连接的所有槽中完全断开信号,请调用信号disconnect all()成员函数:

signal0<> Bang();
Bang.connect(&bomb, &Bomb::Explode);
Bang.connect(&bomb2, &Bomb::Explode);
Bang.connect(&secret_base, &SecretBase::SelfDestruct);
Bang.disconnect_all();
Bang(); // Safely defused!


2.6 完全断开槽的对象

为了便于完全断开实现一个或多个插槽的对象,有槽基类提供了disconnect_all()成员函数的功能。 调用disconnect all()会自动断开所有连接的信号:

class MyClass : public has_slots<>
{
public:
    void OnSpeedChange(double mph);
    void OnBrakesApplied(bool brakestate);
};

MyClass car;
signal1 Speed;
signal2 Brakes;
Speed.connect(&car, &MyClass::OnSpeedChange);
Brakes.connect(&car, &MyClass::OnBrakesApplied);
Speed(50.0); // This one gets through
Brakes(true); // So does this
car.disconnect_all();
Speed(31.5); // This one doesn’t get through


2.7 发送未连接的信号

       发出未连接的信号不是错误, 这是一个有意的设计选择 相反,如果没有连接到一个信号,如果它被发射,信号会被安静地忽略。 没有警告产生,因为这是正确的行为。
      这个决定的基本原理可能并不明显,但在实践中这使得某些种类的应用程序更容易编写。

      考虑一个为字符串实现可视化编辑控件的可重用类:

class StringEdit : public has_slots<>
{
public:
    signal0<> OnReturnPressed;
    signal0<> OnTabPressed;
    signal1 OnKeyPressed;
    signal1 OnTextChanged;

    void SetText(char* text); // Slot
    void ClearText(); // Slot
    ...
};

这个类的一些可能的用途可能会找到所有可用信号。 然而,在很多情况下,一些信号将不会有用 - 因此,可以这样使用

if(OnReturnPressed.is_connected())
{
    OnReturnPressed();
}
纯粹为了避免警告,更简单的调用OnReturnPressed()应该是足够。


3 使用注意事项

sigslot库编写仅需要ISO C++和C++标准库(STD),因此很可能在大多数平台上工作不变,至少在单线程模式下是这样。 在线程安全的情况下使用sigslot目前支持Win32和支持Posix线程的系统(例如大多数Unix,最新的Linux变体,OpenBSD,FreeBSD,Windows 95,98,ME,NT3.51,NT4.0,Win2k,XP等)


3.1 库依赖

在ISO标准的模式下,当前版本的仅依赖于自身和标准模板库和list, 多线程支持需要Win32下的windows.h头文件或OS下的pthreads.h支持Posix线程。


3.2 多线程支持

sigslot库目前支持三种替代线程策略

Single Threaded(单线程策略) 在单线程模式下,库不会尝试跨线程保护其内部数据结构。因此,所有对构造函数,析构函数和信号的调用都是至关重要的必须存在于单个线程内。

Multithreaded Global(多线程策略) 全局在多线程全局模式下,该库使用一个单一的全局临界区来保护其内部数据结构。这种方法在使用方面的开销很小或内存,但由于只有一个关键部分被共享,所以有时可能会进行不必要的阻塞在所有对象之间。

Multithreaded Local(多线程本地) 在多线程本地模式下,库为每个对象使用一个单独的临界区。意味着每个信号都有其自己的关键部分,每个类都从其继承has_slots。这些关键部分仅在绝对必要时锁定,大量多线程应用程序中使用大量信号/槽减少线程竞争但是,这个有一定的代价,因为必须创建非常多的关键部分对象并保持。

有两种选择的方法来设置库的线程模式:全局或每个基础

所有的信号类和槽都带有一个额外的可选参数,它指定了用于该特定类的多线程策略:

// Single-threaded
signal1 Sig1;
// Multithreaded Global
signal1 Sig2;
// Multithreaded Local
signal1 Sig3

虽然应用程序可以自由地在内部使用任何线程模式组合,但这不是一个好主意结合需要彼此互操作的单线程和多线程策略。 这是一这是编译器不会出错的唯一“违规”,因此程序员要小心。然而,混合multi threaded globalmulti threaded local 是允许的,因为两者都是正确的实现锁定语义


3.2.1 全局设置线程模式

       定义预处理器变量SIGSLOT_PURE_ISO强制所有平台上的ISO C++遵从性。 这个关闭线程支持,所以线程模式自动设置为Single_Threaded 如果说开关不存在,库试图找出正在使用的平台。 WIN32被定义,Win32被假定,并且线程支持被启用。 同样,如果__GNUG__被定义,则假定gcc和Posix线程。 如果您在Unix或类Unix操作系统上使用除gcc以外的其他内容,则可以定义SIGSLOT__USE__POSIX__RHREADS来强制使用Posix线程。

      缺省线程模式由SIGSLOT_DEFAULT_MT_POLICY变量设置。 这个如果未定义,则默认为multi threaded global 要全局设置线程模式,请确保在包含sigslot.h之前,SIGSLOT_DEFAULT_MT_POLICY已正确设置。

      默认的线程模式用在线程模式没有明确指定的地方 - 如果是指定,这总是覆盖默认值。


3.2.2 槽的线程安全

sigslot库不会自动保证你的插槽是线程安全的。 你应该假设可以在'不方便的时间'调用插槽,并且应该相应地进行防守编程。

      虽然sigslot不打算成为一个完整的线程库,但它确实包含了一些对于创建一个实现槽线程安全的类非常有用。 has_slots类继承多线程策略,它又提供成员函数lock()和unlock()。 这些函数分别锁定和解锁互斥锁,并用于保护内部数据结构用于实现信号/插槽机制。 你可以自己使用lock()和unlock()代码,

例如:

class MyMultithreadedClass
: public has_slots
{
public:
void Entry1() // Slot
{
    lock();
    ...
    unlock();
}
void Entry2() // Slot
{
    lock();
    ...
    unlock();
}
};

sigslot提供了一个有用的类,它允许关键部分在块范围内自动锁定和解锁:

class MyMultithreadedClass
: public has_slots
{
public:
void Entry1() // Slot
{
    lock_block lock(this);
    ...
}
void Entry2()
{
    lock_block lock(this);
        ...
}
};

当lock_block对象时,它会锁定传入对象所拥有的关键部分。 什么时候锁块对象超出范围,关键部分自动释放


3.3 命名空间

sigslot库将其所有定义放置在sigslot命名空间中。 为了清楚简洁起见,本文档中的例子都假命名空间已经打开,例如:

#include 
using namespace sigslot;
与标准模板库的标准命名空间一样,这是个人选择/或本地编码标准是否明确使用'sigslot ::'或打开命名空间。


你可能感兴趣的:(c/c++,编程)