C++中的信号和槽
sigslot.h 源码及英文文档可以从这里下载https://github.com/EGeeks/sigslot
1 介绍
本文介绍了sigslot库,它使用C++实现类型安全,线程安全的信号/插槽机制。 该库完全使用C ++实现, 并且不需要对源代码进行预处理即可使用。 sigslot库主页http://sigslot.sourceforge.net/ , 先看看那里本文的最新版本,以及库本身的最新下载。
1.1 sigslot库范例
大多数传统的C++代码最终归结为一个(可能很多)类,它们通过调用彼此的成员函数进行互操作。 允许类以这种方式进行交互操作通常需要类相当详细地了解对方。 例如,一个家庭自动化系统可能包含如下几个类
1
2
3
4
5
6
7
8
9
10
11
12class Switch { public: virtual void Clicked() = 0; }; class Light { public: void ToggleState(); void TurnOn(); void TurnOff(); };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class 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的一个本地实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Switch { public: signal0<> Clicked; }; class Light : public has_slots<> { public: void ToggleState(); void TurnOn(); void TurnOff(); }; Switch sw1, sw2; Light lp1, lp2;
Light类在很大程度上没有改变,只是它继承了'has_slots'。
而不是需要实现像ToggleSwitch这样混乱的派生类,现在可以'开关' 连接指示灯:
1
2sw1.Clicked.connect(&lp1, &Light::ToggleState); sw2.Clicked.connect(&lp2, &Light::ToggleState);
使用信号与槽后现在变得很清晰, 现在添加两个lights, 各自有自己的触发开关, 加一个全局的全开全关开关
1
2
3
4
5
6
7
8
9
10
11
12Switch 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 表示参数个数
在下面的例子中,封装窗口的类发送各种信号在窗口被移动,调整大小,打开或关闭:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Window { public: enum WindowState { Minimised, Normal, Maximised }; signal1<WindowState> StateChanged; signal2<int, int> MovedTo; signal2<int, int> 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<char *, int> ReportError;
可以通过调用其函数操作符来使其发出信号。 在实践中,这看起来调用一个函数
ReportError("Something went wrong", ERR_SOMETHING_WRONG);
或者,您可以通过调用emit()成员函数来获得完全相同的结果:
ReportError.emit("Something went wrong", ERR_SOMETHING_WRONG);
2.2 连接信号
当前实现使用STL列表来实现槽连接列表。 意即该槽按与连接顺序相同的顺序调用。 但是可能依靠这个不明智,因为未来调整到sigslot库可能会改变这种行为。
2.3 断开信号
断开信号操作是非常罕见的, 因为离开作用域后自动断开信号,但是如果您需要这样做,您可以调用信号的disconnect()成员函数与目标类的指针:
1
2
3
4
5
6
7
8
9signal1<int> 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()成员函数:
1
2
3
4
5
6signal0<> 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()会自动断开所有连接的信号:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class MyClass : public has_slots<> { public: void OnSpeedChange(double mph); void OnBrakesApplied(bool brakestate); }; MyClass car; signal1<double> Speed; signal2<bool> 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 发送未连接的信号
发出未连接的信号不是错误, 这是一个有意的设计选择。 相反,如果没有连接到一个信号,如果它被发射,信号会被安静地忽略。 没有警告产生,因为这是正确的行为。
这个决定的基本原理可能并不明显,但在实践中这使得某些种类的应用程序更容易编写。
考虑一个为字符串实现可视化编辑控件的可重用类:
1
2
3
4
5
6
7
8
9
10
11class StringEdit : public has_slots<> { public: signal0<> OnReturnPressed; signal0<> OnTabPressed; signal1<char> OnKeyPressed; signal1<char *> OnTextChanged; void SetText(char* text); // Slot void ClearText(); // Slot ... };
这个类的一些可能的用途可能会找到所有可用信号。 然而,在很多情况下,一些信号将不会有用 - 因此,可以这样使用
1
2
3
4if(OnReturnPressed.is_connected()) { 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。这些关键部分仅在绝对必要时锁定,在大量多线程应用程序中使用大量信号/槽减少线程竞争。但是,这个有一定的代价,因为必须创建非常多的关键部分对象并保持。
有两种选择的方法来设置库的线程模式:全局或每个基础类。
所有的信号类和槽都带有一个额外的可选参数,它指定了用于该特定类的多线程策略:
1
2
3
4
5
6// Single-threaded signal1<int, single_threaded> Sig1; // Multithreaded Global signal1<int, multi_threaded_global> Sig2; // Multithreaded Local signal1<int, multi_threaded_local> 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()代码,
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class MyMultithreadedClass : public has_slots<multi_threaded_local> { public: void Entry1() // Slot { lock(); ... unlock(); } void Entry2() // Slot { lock(); ... unlock(); } };
sigslot提供了一个有用的类,它允许关键部分在块范围内自动锁定和解锁:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class MyMultithreadedClass : public has_slots<multi_threaded_local> { public: void Entry1() // Slot { lock_block<multi_threaded_local> lock(this); ... } void Entry2() { lock_block<multi_threaded_local> lock(this); ... } };
当lock_block对象时,它会锁定传入对象所拥有的关键部分。 什么时候锁块对象超出范围,关键部分自动释放
3.3 命名空间
sigslot库将其所有定义放置在sigslot命名空间中。 为了清楚简洁起见,本文档中的例子都假命名空间已经打开,例如:
1
2#include <sigslot.h> using namespace sigslot;
最后
以上就是高兴手链最近收集整理的关于sigslot.h 中文文档的全部内容,更多相关sigslot.h内容请搜索靠谱客的其他文章。
发表评论 取消回复