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();
};
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()'已经消失,被一个信号取代。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("Something went wrong", ERR_SOMETHING_WRONG);
或者,您可以通过调用emit()成员函数来获得完全相同的结果:
ReportError.emit("Something went wrong", ERR_SOMETHING_WRONG);
2.2 连接信号
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 global和multi 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 ::'或打开命名空间。