在日常的程序开发中我们经常会遇到以下的实际问题:
- 比如在一个文件下载完成时,发送邮件或者微信通知告知用户;
- 比如点击一个按钮时,执行相应的业务逻辑;
- 比如当用户的金额少于一个阈值时,通知用户及时充值;
等等。
这些业务需求其实都对应着观察者模式,当一个对象的状态发生改变或者达到某种条件,所有的观察者对象都会得到通知,观察者模式通过面向对象设计,实现软件结构的松耦合设计。
C#中的委托和事件以及Qt的信号和槽机制都是遵循了此种设计模式。在使用C#和Qt的过程中常常感叹为什么C++标准库不自带这种快速开发的原生类呢(虽然boost中有),那么本文我们就使用C++模板实现一个简单但是够用的C++事件工具类。
1 .Net的委托和事件
我们首先看下C#中的委托示例
class Program
{
//1、声明委托类型
public delegate void AddDelegate(int a, int b);
//2、委托函数(方法),参数需要和委托参数一致
public static void Add(int a, int b)
{
Console.WriteLine(a + b);
}
static void Main(string[] args)
{
//3、创建委托实例,将方法名Add作为参数绑定到该委托实例,也可以不使用new,直接AddDelegate addDelegate = Add;
AddDelegate addDelegate = new AddDelegate(Add);
//4、调用委托实例
addDelegate(1, 2);
Console.ReadKey();
}
}
从上述代码可以看出C#的委托是不是与C++的函数指针声明很像,先声明一种表明返回值和形参的函数形式,然后把一个符合这种形式的函数当做参数进行传递,并最后进行调用,类似于C的函数指针声明以及C++的std::function
。
看完委托之后,我们来看一个事件的示例,
public class Account
{
private float bank_savings = 1000; // 存款金额
public event Action OnInsufficientBalance; // 余额不足事件
public void cosume(float money)
{
bank_savings -= money;
if (bank_savings < 100)
{
OnInsufficientBalance.InVoke();
}
}
}
public class Notify
{
public static void Email()
{
Console.WriteLine("Insufficient Balance");
}
}
class Program
{
static void Main(string[] args)
{
var account = new Account();
account.OnInsufficientBalance += Notify.Email;
account.cosume(1000);
}
}
在上述代码中我们声明一个OnInsufficientBalance
事件,这个事件在用户账户低于100的时候触发,触发函数使用邮件告知用户。
2 Qt的信号和槽
Qt的信号和槽机制是由Qt实现的观察者机制,可以通过信号触发绑定的槽方法。
信号(Signal)就是在特定情况下被发射的事件,例如 PushButton 最常见的信号就是鼠标单击时发射的 clicked() 信号。
槽(Slot)就是对信号响应的函数。槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行。
当点击一个按钮时,Qt发出按钮被点击的信号,然后触发信号绑定的开发者的自定义槽方法。
Qt的信号和槽方法与.Net的委托和事件大致相同,其中信号对应事件,槽函数对应委托。
示例代码如下:
button1 = new QPushButton("close",this);//创建按钮,指定父对象
button2 = new QPushButton("print",this);//创建按钮,指定父对象
connect(button1,&QPushButton::clicked,this,&QWidget::close);
connect(button2,&QPushButton::clicked,this,[](){
qDebug() << "关闭成功";//打印关闭成功
});
3 Duilib中委托和事件
在Duilib也有对委托和事件的简单实现,我们可以在UIDelegate.h
和UIDelegate.cpp
中看到相应的实现。
UIDelegate.h
#ifndef __UIDELEGATE_H__
#define __UIDELEGATE_H__
#pragma once
namespace DuiLib {
class DUILIB_API CDelegateBase
{
public:
CDelegateBase(void* pObject, void* pFn);
CDelegateBase(const CDelegateBase& rhs);
virtual ~CDelegateBase();
bool Equals(const CDelegateBase& rhs) const;
bool operator() (void* param);
virtual CDelegateBase* Copy() const = 0; // add const for gcc
protected:
void* GetFn();
void* GetObject();
virtual bool Invoke(void* param) = 0;
private:
void* m_pObject;
void* m_pFn;
};
class CDelegateStatic: public CDelegateBase
{
typedef bool (*Fn)(void*);
public:
CDelegateStatic(Fn pFn) : CDelegateBase(NULL, pFn) { }
CDelegateStatic(const CDelegateStatic& rhs) : CDelegateBase(rhs) { }
virtual CDelegateBase* Copy() const { return new CDelegateStatic(*this); }
protected:
virtual bool Invoke(void* param)
{
Fn pFn = (Fn)GetFn();
return (*pFn)(param);
}
};
template <class O, class T>
class CDelegate : public CDelegateBase
{
typedef bool (T::* Fn)(void*);
public:
CDelegate(O* pObj, Fn pFn) : CDelegateBase(pObj, *(void**)&pFn) { }
CDelegate(const CDelegate& rhs) : CDelegateBase(rhs) { }
virtual CDelegateBase* Copy() const { return new CDelegate(*this); }
protected:
virtual bool Invoke(void* param)
{
O* pObject = (O*) GetObject();
union
{
void* ptr;
Fn fn;
} func = { GetFn() };
return (pObject->*func.fn)(param);
}
private:
Fn m_pFn;
};
template <class O, class T>
CDelegate<O, T> MakeDelegate(O* pObject, bool (T::* pFn)(void*))
{
return CDelegate<O, T>(pObject, pFn);
}
inline CDelegateStatic MakeDelegate(bool (*pFn)(void*))
{
return CDelegateStatic(pFn);
}
class DUILIB_API CEventSource
{
typedef bool (*FnType)(void*);
public:
~CEventSource();
operator bool();
void operator+= (const CDelegateBase& d); // add const for gcc
void operator+= (FnType pFn);
void operator-= (const CDelegateBase& d);
void operator-= (FnType pFn);
bool operator() (void* param);
protected:
CDuiPtrArray m_aDelegates;
};
} // namespace DuiLib
#endif // __UIDELEGATE_H__
UIDelegate.cpp
#include "StdAfx.h"
namespace DuiLib {
CDelegateBase::CDelegateBase(void* pObject, void* pFn)
{
m_pObject = pObject;
m_pFn = pFn;
}
CDelegateBase::CDelegateBase(const CDelegateBase& rhs)
{
m_pObject = rhs.m_pObject;
m_pFn = rhs.m_pFn;
}
CDelegateBase::~CDelegateBase()
{
}
bool CDelegateBase::Equals(const CDelegateBase& rhs) const
{
return m_pObject == rhs.m_pObject && m_pFn == rhs.m_pFn;
}
bool CDelegateBase::operator() (void* param)
{
return Invoke(param);
}
void* CDelegateBase::GetFn()
{
return m_pFn;
}
void* CDelegateBase::GetObject()
{
return m_pObject;
}
CEventSource::~CEventSource()
{
for( int i = 0; i < m_aDelegates.GetSize(); i++ ) {
CDelegateBase* pObject = static_cast<CDelegateBase*>(m_aDelegates[i]);
if( pObject) delete pObject;
}
}
CEventSource::operator bool()
{
return m_aDelegates.GetSize() > 0;
}
void CEventSource::operator+= (const CDelegateBase& d)
{
for( int i = 0; i < m_aDelegates.GetSize(); i++ ) {
CDelegateBase* pObject = static_cast<CDelegateBase*>(m_aDelegates[i]);
if( pObject && pObject->Equals(d) ) return;
}
m_aDelegates.Add(d.Copy());
}
void CEventSource::operator+= (FnType pFn)
{
(*this) += MakeDelegate(pFn);
}
void CEventSource::operator-= (const CDelegateBase& d)
{
for( int i = 0; i < m_aDelegates.GetSize(); i++ ) {
CDelegateBase* pObject = static_cast<CDelegateBase*>(m_aDelegates[i]);
if( pObject && pObject->Equals(d) ) {
delete pObject;
m_aDelegates.Remove(i);
return;
}
}
}
void CEventSource::operator-= (FnType pFn)
{
(*this) -= MakeDelegate(pFn);
}
bool CEventSource::operator() (void* param)
{
for( int i = 0; i < m_aDelegates.GetSize(); i++ ) {
CDelegateBase* pObject = static_cast<CDelegateBase*>(m_aDelegates[i]);
if( pObject && !(*pObject)(param) ) return false;
}
return true;
}
} // namespace DuiLib
从上述Duilib实现委托与事件机制的源码,我们可以看出整个的实现思路,通过CEventSource
创建事件,通过MakeDelegate
函数构建绑定到事件上的委托函数CDelegate<O, T>
,而这种委托函数的形式只能是void(void*)
的形式。然后通过CEventSource
重载操作符+=
和-=
添加和删除委托函数。Duilib这种方式应该就是最简单的事件和委托的原型,但是缺点是事件只能绑定固定形式的委托函数。
4 使用C++标准库简单实现事件触发机制
第3节Duilib的委托和事件不能自定义事件所绑定委托函数的形式,在本节中我们使用C++标准库对事件机制进行实现,可以自定义事件绑定函数的形式。
具体的代码如下:
Event.hpp
#ifndef _EVENT_H_
#define _EVENT_H_
#include <vector>
#include <functional>
#include <type_traits>
#include <memory>
#include <assert.h>
namespace stubbornhuang
{
// 原型
template<typename Prototype> class Event;
// 特例
template<typename ReturnType, typename ...Args>
class Event <ReturnType(Args...)>
{
private:
using return_type = ReturnType;
using function_type = ReturnType(Args...);
using stl_function_type = std::function<function_type>;
using pointer = ReturnType(*)(Args...);
private:
class EventHandler
{
public:
EventHandler(stl_function_type func)
{
assert(func != nullptr);
m_Handler = func;
}
void Invoke(Args ...args)
{
if (m_Handler != nullptr)
{
m_Handler(args...);
}
}
private:
stl_function_type m_Handler;
};
public:
void operator += (stl_function_type func)
{
std::shared_ptr<EventHandler> pEventHandler = std::make_shared<EventHandler>(func);
if (pEventHandler != nullptr)
{
m_HandlerVector.push_back(std::move(pEventHandler));
}
}
void Connect(stl_function_type func)
{
std::shared_ptr<EventHandler> pEventHandler = std::make_shared<EventHandler>(func);
if (pEventHandler != nullptr)
{
m_HandlerVector.push_back(std::move(pEventHandler));
}
}
void operator() (Args ...args)
{
for (int i = 0; i < m_HandlerVector.size(); ++i)
{
if (m_HandlerVector[i] != nullptr)
{
m_HandlerVector[i]->Invoke(args...);
}
}
}
void Trigger(Args ...args)
{
for (int i = 0; i < m_HandlerVector.size(); ++i)
{
if (m_HandlerVector[i] != nullptr)
{
m_HandlerVector[i]->Invoke(args...);
}
}
}
private:
std::vector<std::shared_ptr<EventHandler>> m_HandlerVector;
};
}
#endif // !_EVENT_H_
在上述代码中我们使用template<typename ReturnType, typename ...Args>
对事件类Event
进行了模板化,使用变参模板typename ...Args
自定义事件绑定的委托函数参数列表,可以接受多个不同类型的参数。使用std::vector
存储绑定事件的std::function<ReturnType(Args...)>
的委托函数,并重载+=
操作符添加委托函数。
上述事件工具类Event
的使用示例如下:
#include <iostream>
#include "Event.h"
class Button
{
public:
Button()
{
}
virtual~Button()
{
}
public:
stubbornhuang::Event<void()> OnClick;
};
void Click()
{
std::cout << "Button Click" << std::endl;
}
class Example
{
public:
void Click()
{
std::cout << "Example Click" << std::endl;
}
};
int main()
{
Button button;
button.OnClick += Click; // 静态函数做委托函数
Example example;
button.OnClick += std::bind(&Example::Click, example); // 成员函数做委托函数
button.OnClick += []() { std::cout << "Lambda Click" << std::endl; }; // 匿名函数做委托函数
button.OnClick();
return 0;
}
执行结果:
Button Click
Example Click
Lambda Click
由于std::function
的超强特性,我们可以为事件绑定静态函数、类成员函数以及匿名函数。
5 总结
在本文中,我们对.Net的事件和委托,Qt的信号和槽进行了简单的介绍,然后通过引入Duilib中对于事件和委托的简单实现,进而扩展了自定义的简单事件类Event
,此类实现的比较简单,但是包含了事件实践的核心思想,自己对于模板类,以及变参模板的使用又有了新的体会。
6 整理修改之后的开源项目
最近有时间,在第4小节的基础上,对代码进行了扩展和整理,并在Github上进行了开源,项目地址为:https://github.com/HW140701/TinyEvent,有兴趣的同学可以Star或者Fork。
修改之后的Event事件类如下
/*
Author:StubbornHuang
Data:2023.1.31
Email:stubbornhuang@qq.com
*/
#ifndef _EVENT_H_
#define _EVENT_H_
#include <functional>
#include <map>
#include <type_traits>
#ifndef EVENT_NO_THREAD_SAFETY
#define EVENT_THREAD_SAFETY
#endif // !EVENT_NO_THREAD_SAFETY
#ifdef EVENT_THREAD_SAFETY
#include <atomic>
#include <mutex>
#endif // EVENT_THREAD_SAFETY
#ifdef EVENT_THREAD_SAFETY
#define DELEGATE_ID_TYPE std::atomic_uint64_t
#else
#define DELEGATE_ID_TYPE std::uint64_t
#endif // EVENT_THREAD_SAFETY
namespace stubbornhuang
{
static DELEGATE_ID_TYPE DELEGATE_ID = 1;
template<typename Prototype> class Event;
template<typename ReturnType, typename ...Args>
class Event <ReturnType(Args...)>
{
private:
using return_type = ReturnType;
using function_type = ReturnType(Args ...);
using std_function_type = std::function<function_type>;
using function_pointer = ReturnType(*)(Args...);
private:
class Delegate
{
public:
Delegate() = delete;
Delegate(int id,std_function_type std_function_func)
:m_Handler(nullptr),m_Id(-1)
{
if (std_function_func == nullptr)
return;
m_Id = id;
m_Handler = std_function_func;
}
void Invoke(Args ...args)
{
if (m_Handler != nullptr)
{
m_Handler(args...);
}
}
private:
int m_Id;
std_function_type m_Handler;
};
public:
int AddDelegate(std_function_type std_function_func)
{
if (std_function_func == nullptr)
return -1;
std::shared_ptr<Delegate> pDelegate = std::make_shared<Delegate>(DELEGATE_ID, std_function_func);
#ifdef EVENT_THREAD_SAFETY
std::lock_guard<std::mutex> guard_mutex(m_EventMutex);
#endif // EVENT_THREAD_SAFETY
m_Delegates.insert(std::pair<int, std::shared_ptr<Delegate>>(DELEGATE_ID, pDelegate));
return DELEGATE_ID++;
}
bool RemoveDelegate(int delegate_id)
{
#ifdef EVENT_THREAD_SAFETY
std::lock_guard<std::mutex> guard_mutex(m_EventMutex);
#endif // EVENT_THREAD_SAFETY
if (m_Delegates.count(delegate_id) == 0)
return false;
m_Delegates.erase(delegate_id);
return true;
}
int operator += (std_function_type std_function_func)
{
return AddDelegate(std_function_func);
}
bool operator -= (int delegate_id)
{
return RemoveDelegate(delegate_id);
}
void Invoke(Args ...args)
{
#ifdef EVENT_THREAD_SAFETY
std::lock_guard<std::mutex> guard_mutex(m_EventMutex);
#endif // EVENT_THREAD_SAFETY
for (const auto& key : m_Delegates)
{
key.second->Invoke(args...);
}
}
bool Invoke(int delegate_id, Args ...args)
{
#ifdef EVENT_THREAD_SAFETY
std::lock_guard<std::mutex> guard_mutex(m_EventMutex);
#endif // EVENT_THREAD_SAFETY
if (m_Delegates.count(delegate_id) == 0)
return false;
m_Delegates[delegate_id]->Invoke(args...);
return true;
}
private:
std::map<int, std::shared_ptr<Delegate>> m_Delegates;
#ifdef EVENT_THREAD_SAFETY
std::mutex m_EventMutex;
#endif // EVENT_THREAD_SAFETY
};
}
#endif // !_EVENT_H_
该类默认是线程安全的,如果不想要线程安全,可以在包含头文件之前定义EVENT_NO_THREAD_SAFETY
即可,该类同时也是模板类,支持定义任何返回类型和可变参数的事件类,并通过返回委托函数ID,对委托函数进行删除操作。
本文作者:StubbornHuang
版权声明:本文为站长原创文章,如果转载请注明原文链接!
原文标题:C++ – 使用标准库实现事件和委托,信号和槽机制
原文链接:https://www.stubbornhuang.com/2404/
发布于:2022年11月02日 10:38:32
修改于:2023年06月21日 17:54:07
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
评论
50