C++ - 標準ライブラリを使用してイベント、デリゲート、シグナル、スロット メカニズムを実装する

日々のプログラム開発では、次のような実際的な問題に遭遇することがよくあります。

  • たとえば、ファイルのダウンロードが完了したら、電子メールまたは WeChat 通知を送信してユーザーに通知します。
  • たとえば、ボタンをクリックすると、対応するビジネス ロジックが実行されます。
  • たとえば、ユーザーの金額がしきい値未満の場合、ユーザーは時間内に再チャージするように通知されます。

これらのビジネス要件は、実際にはオブザーバー パターンに対応しており、オブジェクトの状態が変化したり、ある条件に達すると、すべてのオブザーバー オブジェクトに通知され、オブジェクト指向設計によるソフトウェア構造の疎結合設計を実現します。

C# のデリゲートとイベント、および Qt のシグナルとスロット メカニズムはすべて、この設計パターンに従います。C# と Qt を使用する過程で、なぜ C++ 標準ライブラリには迅速な開発のためのこの種のネイティブ クラスが付属していないのか (Boost には付属していますが) 嘆くことがよくあるため、この記事では C++ テンプレートを使用して、シンプルだが十分な C++ を実装します。イベントツールクラス。

1. ネット参加者とイベント

まずは 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 によって実装されたオブザーバー メカニズムであり、シグナルを通じてバインドされたスロット メソッドをトリガーできます。

シグナルとは、特定の状況下で発行されるイベントです。たとえば、PushButton の最も一般的なシグナルは、マウスがクリックされたときに発行される clicked() シグナルです。
スロットは信号に応答する関数です。スロット関数は信号に関連付けることができ、信号が送信されると、関連付けられたスロット関数が自動的に実行されます。

ボタンがクリックされると、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 にはデリゲートとイベントの単純な実装もあり、対応する実装は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イベントにバインドされたデリゲート関数が function を通じて構築されCDelegate<O, T>、このデリゲート関数の形式は次のとおりです。void(void*)の形式になります。次に、CEventSource演算子をオーバーロードし+=-=デリゲート関数を追加および削除します。Duilib のメソッドは最も単純なイベントとデリゲートのプロトタイプであるはずですが、欠点はイベントが固定形式のデリゲート関数にのみバインドできることです。

4 C++標準ライブラリを利用したイベントトリガー機構の簡易実装

セクション 3 Duilib のデリゲートとイベントは、イベントにバインドされたデリゲート関数の形式をカスタマイズできません。このセクションでは、C++ 標準ライブラリを使用してイベント メカニズムを実装し、イベント バインディング関数の形式をカスタマイズできます。

具体的なコードは次のとおりです。

イベント.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 です。興味のある学生はスターまたはフォークしてください

変更されたEventイベントクラスは以下のとおりです。

/*
Author:StubbornHuang
Data:2023.1.31
Email:[email protected]
*/


#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 を返し、デリゲート関数を削除します。

ご興味がございましたら、私の個人ウェブサイトをご覧ください: https://www.stubbornhuang.com/

おすすめ

転載: blog.csdn.net/HW140701/article/details/127647414