在C++ 中使用信号和槽(Signals and Slots)

简介

本文介绍sigslot这个库, 它是用C++实现的具有类型安全,线程安全的信号和槽机制的库。它完全使用C++语言进行编写,只有一个头文件,因此避免了在使用的时候进行源码的预编译。sigslot的主页是http://sigslot.sourceforge.net。

一个Signal/Slot的例子

在传统的C++代码中类之间的沟通是必须通过调用彼此的成员方法,这也就使得每个类要在定义的时候就要指定调用的成员方法以及其说所属的类进行。例如:

class Switch {
public:
virtual void Clicked() = 0;
};
class Light {
public:
void ToggleState();
void TurnOn();
void TurnOff();
};

在不改变Switch或Light类的情况下,让Switch类的Clicked方法控制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类中不需要考虑控制这个灯使用的什么样的开关。
下边我们就使用库sigslot进行Switch和Light类的改造。

class Switch {
public:
sigslot::signal0<> Clicked;
};
class Light : public sigslot::has_slots<> {
public:
void ToggleState();
void TurnOn();
void TurnOff();
};
Light lp1, lp2;
Switch sw1, sw2;

主要改动

  1. 在Switch类中纯虚函数Clicked(), 被一个类型为sigslot::signal0<>的成员方法取代。
  2. 在Lightl类中Light类继承于类sigslot::has_slots<>.

修改之后如果希望让Switch控制Light的状态只需要使用下边的方法将Switch的实例和Light的实例进行连接。

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

如果你想再加两盏灯而且每个灯也都有自己的开完,我们也再添加两个开关,一个开关可以关闭所有的灯,一个可以打开所有的灯,我们也只需要进行简单的修改。

Light lp3, lp4;
Switch sw3, sw4, all_on, all_off;
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());

 参数类型

Signal和Slots也是可以带一个或多个指定类型的参数。库sigslot是建立在C++的templated机制上的,所以在signal和solts的声明和调用的时候都会进行完全的类型检查。
signal类型的命名规则是signaln n是参数的个数。
在下边的这个例子中,windows在状态发生变化的时候就会发出一系列的信号,在MyControl的实例对象中就可以对这些信号进行处理。

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);

信号signal和槽slot的命名不是关键的,主要是singal和slot的参数类型和个数要一致。

库的使用

发送信号

例如信号
signal2 ReportError;
我们使用如下两种方法来发送这个信号

1. ReportError("Something went wrong", ERR_SOMETHING_WRONG);
2. ReportError.emit("Something went wrong", ERR_SOMETHING_WRONG);

方法1是在类signal2<> 中对() 的重载实际调用的还是方法emit()

连接信号和槽

信号的连接是使用方法类signaln<>的成员方法connect().方法connect()需要指定两个参数
1 指向目标实例的指针
2 指向目标实例的成员方法的指针

为了让方法connect()能够工作,需要目标实例的类是继承于类has_slots.
一个信号是可以连接到任意数量的槽上的,当信号被发送的时候,所以连接到这个信号上的槽都会被调用。槽的调用对信号是无感的,所以槽方法的返回值都是void。
在现在的实现中槽方法是使用std::list进行存储的,这也就意味着当一个信号连接了多个槽的时候,槽方法的调用顺序和其被连接的顺序是一致的。在未来的版本中可能会对这个行为进行修改,因此我们不能确保槽方法的调用顺序一定会和其被连接的顺序一致。

断开信号和槽的连接

我们很少会用到断开信号的操作, 因为当信号实体或者槽实体被析构的时候,信号与槽的连接也就被断开了。但是在需要的时候我们还是可以调用方法signal的成员方法disconnec(&)来断开连接。
例如:

w.Resized.disconnect(&c); 

断开w的Resized信号和槽方法MyControl::OnResize的连接。

槽方法的实现

槽方法是具有如下属性的普通成员方法

  1. 返回类型必须是void
  2. 携带0到8个任意类型的参数
  3. 必须继承于has_slots<>

槽方法可以在信号槽机制中被调用,也可以作为普通方法被调用。

断开信号的所用连接

signal的成员方法disconnec_all()可以用来断开与该信号连接的所有槽
如:

all_on.Clicked.disconnec_all();
all_off.Clicked.disconnec_all();

此后开关all_on将不能打开所有灯。

断开槽对象中所用连接

has_slots的成员方法disconnec_all()可以用来断开与该目标对象连接的所有信号

lp3.disconnec_all();

此后灯lp3将不受开关控制

发送一个槽连接的信号

信号会将被忽略,不会有错误生成,这样可以简化程序的编写。
如下所示一个可重用的字符串可视化编辑控件类

class StringEdit : public has_slots<> {
public:
signal0<> OnReturnPressed;
signal0<> OnTabPressed;
signal1 OnKeyPressed;
signal1 OnTextChanged;
void SetText(char* text); // Slot
void ClearText(); // Slot
...
};

没有错误发生所以每个信号在发送的时候就不用进行烦人的is_connected检查。

信号的线程安全

signal提供了三种信号安全模式

1 single_threaded

  单线程没有锁的保护

2 multi_threaded_global

  所有信号共享同一个锁,锁的创建消耗小,但是锁的粒度大

3 multi_threaded_local

  每个信号都有一个锁,锁的创建开销大,但是粒度小

使用方法如下,通过Template的最后一个参数指定。

// Single-threaded 
signal1 Sig1;

// Multithreaded Global 
signal1 Sig2;

// Multithreaded Local 
signal1 Sig3

槽的线程安全

槽函数中的线程安全需要开发者自己来维护但是signal也提供了两个可用的锁

1:

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

2:

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

void Entry2()
{
lock_block lock(this); 
...
}
}

 

你可能感兴趣的:(WebRTC)