저번 글에서 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를 사용하는 것이 더 유리해 보입니다(랜덤 액세스가 가능하니 속도도 더 빠름).
#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++로 간단히 배껴서 만들어 본 마우스 이벤트 데모 예제입니다.
#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요소 등 이벤트 시스템이 유용한 여러 분야에 사용할 수 있습니다.

최신 댓글