template + lambda의 편리함
글쓴이: klara / 작성시간: 일, 2013/08/11 - 7:50오후
대부분의 어플리케이션에 빠지지 않는 기능이 undo/redo(실행 취소/다시 실행)기능일 겁니다.
C++에서 이걸 구현하는 일반적인 방법은 실행 단위가 되는 클래스를 만들어 stack으로 관리하는 걸겁니다.
예를 들어, 다음과 같은 Application 클래스가 있다고 합시다.
#include <string> class Application { public: Application() {} void setTitle(const std::string &title) { m_title = title; updateWindowTitle(); } void setCount(int count) { m_count = count; } void updateWindowTitle(); private: std::string m_title; int m_count = 0; };
두가지 값을 바꾸는 기능이 있는데, 여기에 undo/redo를 도입하면 다음과 같은 형태가 될 것입니다.
#include <string> #include <vector> class UndoCommand { public: UndoCommand() {} virtual ~UndoCommand() {} virtual void undo() = 0; virtual void redo() = 0; }; class TitleCommand : public UndoCommand { public: TitleCommand(std::string *title, const std::string &to, Application *app) : m_title(title), m_to(to), m_from(*title), m_app(app) {} virtual void undo() override { *m_title = m_from; m_app->updateWindowTitle(); } virtual void redo() override { *m_title = m_to; m_app->updateWindowTitle(); } private: std::string *m_title, m_to, m_from; Application *m_app; }; class CountCommand : public UndoCommand { public: CountCommand(int *count, int to) : m_count(count), m_to(to), m_from(*count) {} virtual void undo() override { *m_count = m_from; } virtual void redo() override { *m_count = m_to; } private: int *m_count, m_to, m_from; }; class Application { public: Application() {} ~Application() { for (auto cmd : m_cmds) delete cmd; } void setTitle(const std::string &title) { push(new TitleCommand(&m_title, title)); } void setCount(int count) { push(new CountCommand(&m_count, count)); } void undo() { if (!m_cmds.empty() && m_undone != m_cmds.begin()) (*--m_undone)->undo(); } void redo() { if (m_undone != m_cmds.end()) (*m_undone++)->redo(); } void updateWindowTitle(); private: // 새 명령을 추가 void push(UndoCommand *cmd) { cmd->redo(); // 일단 명령 실행해주고 m_cmds.erase(m_undone, m_cmds.end()); // 마지막으로 undo/redo 한 거 이후로 다 지우고 m_cmds.push_back(cmd); // 명령 추가해주고 m_undone = m_cmds.end(); // 마지막 undo 위치 갱신 } std::string m_title; int m_count = 0; std::vector<UndoCommand*> m_cmds; std::vector<UndoCommand*>::iterator m_undone = m_cmds.end(); // 마지막으로 undo된 위치 };
위 코드는 함수 구현이 전부 클래스 안에 들어 있어서 실제로 컴파일이 되지 않습니다.
지금은 어떤 framework같은걸 만드는게 목적이 아니기 때문에 일단 흐름만 파악해주세요.
UndoCommand 는 각각의 명령을 추상화한 기본 클래스입니다.
이경우에는 두가지 명령(TitleCommand와 CountCommand)을 클래스를 만들어 주고, 이들을 std::vector m_cmds로 관리해줍니다.
이제 명령을 실행할 때마다 UndoCommand의 객체들이 생성되어 m_cmds로 들어가고, undo/redo할때마다 m_cmds안에서 적당한 명령을 찾아서 undo/redo 해줍니다.
이것만으로도 꽤 길어졌는데, TitleCommand와 CountCommand 이 클래스들을 보면 매우 비슷하게 생겼습니다.
중복되는 부분이 너무 많아 보입니다. 게다가 어떻게 봐도 TitleCommand와 CountCommand는 재활용가능한 클래스가 아닙니다.
사실 undo/redo에서 실행되는 내용들은 보통 하나의 메뉴에 대응되기 때문에 재활용가능한 코드가 아니고 매우 구체적인 경우가 보통입니다.
손이 근질 거립니다. template을 이용하면 어떻게 하나로 묶어서 재활용 가능한 클래스로 만들 수 있을 거 같습니다.
하지만 undo()와 redo()구현에서 값을 설정한 이후에 업데이트해야 할 내용이 다르기 때문에 쉽지 않습니다.
여기에서는 lambda를 이용하면 다음과 같이 매우 간결한 구현이 만들어집니다.
template<typename T, typename Setter> class ValueCommand : public UndoCommand { public: ValueCommand(const T &to, const T &from, const Setter &setter) : m_to(to), m_from(from), set(setter) {} virtual void undo() override { set(m_from); } virtual void redo() override { set(m_to); } private: T m_to, m_from; Setter set; }; class Application { ... void setTitle(const std::string &title) { push(m_title, title, [this] (const std::string &v) { m_title = v; updateWindowTitle(); }); } void setCount(int count) { push(m_count, count, [this] (int v) { m_count = v; }); } ... private: template<typename T, typename Setter> void push(const T &to, const T &from, const Setter &setter) { push(new ValueCommand<T, Setter>(to, from, setter)); } ... };
여기에서는 명령이 2개뿐이기 때문에 별 차이가 없어보이지만 명령이 늘어나게되면 코드량에 상당한 차이가 나타납니다.
이처럼 lambda와 template을 활용하면 부분적으로 알고리즘이 다른 경우에도 하나의 클래스(또는 함수)로 통합하는게 가능합니다.
lambda에는 이보다 더 강력한 활용예도 많겠지만, 오늘 몇년만에 undo/redo를 다시 구현하다가 lambda+template의 조합을 써보고 참 편리해졌다고 느껴서 적어봤습니다.
Forums:
좋은 강좌 감사합니다! :-)
좋은 강좌 감사합니다! :-)
댓글 달기