C++ 라이브러리 / 이벤트

저번 글에서 delegate 라이브러리를 만들어 보았습니다. 이건 std::fucntion을 대체하기 위한 목적이 컸었는데, 이번엔 다양한 이벤트 기반 프로그래밍에 사용될 수 있도록 멀티 캐스트 기능을 추가해 봅시다. 그전에 여기서의 멀티 캐스트란 하나의 delegate에서 여러개의 바인딩 된 함수를 호출하는 것을 말합니다. 보통 모든 함수의 리턴값을 일일히 처리할 순 없으므로 void형 리턴 값을 가지는 함수만 바인딩 합니다. 멀티 캐스트 기능을 추가하는 방법으로 이전에 만든 delegate를 수정하는 것이 아니라 새로운 Event 클래스를 만들어 봤습니다.

처음으로 고려해야 할 부분은 이벤트가 사용되지 않을 때 최대한 용량을 줄이는 것입니다. 이때 Event의 크기는 포인터 하나와 동일한 8바이트가 되는게 가장 이상적입니다(x64 기준, 앞으로도 계속). 이를 위해 std::unique_ptr<> 스마트 포인터를 활용해서 바인딩 된 delegate가 하나라도 있을 때 delegate를 담는 컨테이너를 하나 할당하는 방식으로 구현합니다(std::unique_ptr은 포인터 하나와 크기가 같음). 여기서 중요한 부분이 delegate를 담는 컨테이너를 무엇으로 하는가 인데, STL에선 메모리를 가장 적게 잡아먹는 컨테이너들인 std::vector나 std::forward_list가 가장 적합합니다. std::vector는 24바이트에 추가로 (delegate 개수) x 16바이트가 필요합니다. std::forward_list는 16바이트에 추가로 (delegate 개수) x 24바이트가 필요합니다. 직접 비교해 보니 std::vector를 사용하는 것이 더 유리해 보입니다(랜덤 액세스가 가능하니 속도도 더 빠름).

event.hpp
#pragma once

#include <memory>
#include <vector>
#include "delegate.hpp" // https://www.dandevlog.com/all/programming/351/

template <class... Args>
class Event {
	using list_t      = std::vector<Delegate<void(Args...)>>;
	using list_iter_t = typename list_t::iterator;

public:
	inline void operator+=(const Delegate<void(Args...)>& handler) {
		if (!list_ptr) list_ptr = std::make_unique<list_t>();
		assert(!has(handler));
		list_ptr->emplace_back(handler);
	}

	inline void operator-=(const Delegate<void(Args...)>& handler) {
		auto iter = find(handler);
		assert(iter != list_ptr->end());
		list_ptr->erase(iter);
		if (list_ptr->empty()) list_ptr.reset();
	}

	inline void invoke(Args... args) const {
		for (auto& handler : *list_ptr)
			handler(std::forward<Args>(args)...);
	}

	inline bool has(const Delegate<void(Args...)>& handler) const {
		if (list_ptr->empty()) return false;
		return find(handler) != list_ptr->end();
	}
	
	inline bool empty() const {
		return !(bool)list_ptr;
	}

	inline operator bool() {
		return (bool)list_ptr;
	}

private:
	inline list_iter_t find(const Delegate<void(Args...)>& handler) const {
		assert(list_ptr);
		return std::find(list_ptr->begin(), list_ptr->end(), handler);
	}

	std::unique_ptr<list_t> list_ptr;
};

// delete lines below if don't needed
class EventSender abstract {};

struct EventArgs {};

typedef Event<EventSender&, EventArgs&> EventHandler;

흠… 만들어 놓고 보니 라이브러리라 하기도 민망할 정도로 간단합니다. 사용법은 간단히 operator+=으로 delegate를 추가하고 operator-=으로 등록된 delegate를 제거합니다. 아래는 C#의 WPF나 윈폼의 이벤트 시스템을 C++로 간단히 배껴서 만들어 본 마우스 이벤트 데모 예제입니다.

main.cpp
#define WIN32_LEAN_AND_MEAN

#include <Windows.h>
#include <iostream>
#include "event.hpp"

using namespace std;

struct MouseEventArgs : public EventArgs {
	MouseEventArgs(double x, double y)
		: x(x), y(y) {}

	double x, y;
	// you can add wheel or buttons etc...
};

typedef Event<EventSender&, MouseEventArgs&> MouseEventHandler;

class Mouse : public EventSender {
public:
	Mouse() {
		GetCursorPos(&pos_before);
	}

	void update() {
		POINT pos;
		GetCursorPos(&pos);

		if (pos.x != pos_before.x || pos.y != pos_before.y) {
			MouseEventArgs args = { (double)pos.x, (double)pos.y };
			if (Event_Move) Event_Move.invoke(*this, args);
			pos_before = pos;
		}
	}

	MouseEventHandler Event_Move;
	// MouseEventHandler Event_Wheel; // you can make whatever you want
	// MouseEventHandler Event_Button;

private:
	POINT pos_before;
};

class Application {
public:
	Application() :
		exit(false) {
		mouse.Event_Move += make_delegate(this, &Application::OnMouseMove);
		mouse.Event_Move += make_delegate(this, &Application::CheckExit);
	}

	bool update() {
		if (exit) return false;
		mouse.update();
		// keyboard.update();
		// ...
		return true;
	}

private:
	void OnMouseMove(EventSender& sender, MouseEventArgs& args) {
		cout << "Mouse Moved! x=" << args.x << ", y=" << args.y << "\n";
	}

	void CheckExit(EventSender& sender, MouseEventArgs& args) {
		if (args.x == 0 && args.y == 0) exit = true;
	}

	bool exit;
	Mouse mouse;
};

int main() {
	cout << "mouse event demo, place cursor x=0, y=0 to exit\n";
	Application app;
	Sleep(10000);
	while (app.update())
		Sleep(10);

	return 0;
}

위 데모를 설명하기 앞서 말하자면 C#에선 Object 클래스를 언어에 존재하는 char, bool, int를 비롯해 임의의 클래스나 모든 데이터형이 부모로 가집니다. 따라서 윈폼이나 WPF의 이벤트 핸들러는 다음과 같은 형식을 가집니다.

void 이벤트_핸들러_이름(Object sender, EventArgs args)

여기서 인자인 sender에 이벤트를 보낸 무언가가 위치해서 핸들러 내부에서 형변환해서 쓰고(null로 지정할 수도 있음) args는 이벤트의 종류에 따라 EventArgs를 상속하는 다른 타입이 될 수 있는데(MouseEventArgs, KeyEventArgs 등) 핸들러에 전달할 추가 데이터가 위치합니다. 근데 왜 뜬금없이 C#타령이냐 하면… 이런 방식으로 GUI같은 이벤트가 중요한 프로그래밍을 하면 느낌상으로 이보다 깔끔한 방법이 없었기 때문입니다. 그래서 C++에서 최대한 비슷하게 따라해 봤습니다.

다시 위 데모로 돌아와서 Mouse 클래스를 보면 이벤트를 등록할 수 있는 멤버인 Event_onMove를 public으로 노출하고 있습니다. 47, 48번 줄에서 Application이 여기에 +=으로 자신의 멤버함수들을(하나는 커서의 위치를 출력하기 위함이고, 나머지 하나는 종료조건을 검사하기 위함) 핸들러로 등록해서 마우스가 움직일 때 마다 이벤트를 처리합니다. 여기서 두개의 핸들러가 동시에 등록되고 호출된다는 점에서 멀티 캐스트 특성이 드러납니다. 데모를 작동시켜 보면 아래처럼 마우스 위치가 변할 때 마다 새로운 위치를 출력하는 것을 볼 수 있습니다. 0, 0으로 마우스를 이동시키면 데모가 종료됩니다. 이를 응용해서 타이머나 키보드, 창, UI요소 등 이벤트 시스템이 유용한 여러 분야에 사용할 수 있습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다