바퀴를 발명하지 말라는 격언이 있지만 공부할 때는 바퀴를 다시 발명해 보는 것도 좋은 방법일 수 있다. Qt의 Signals & Slots를 c++에서 쉽게 구현할 수 있는 방법이 있을까 궁금해서 찾다가 15년이 다 돼가는 아주 오래된 글이지만 옷깃으로 눈물을 훔칠 만큼 감동적인 글을 보게 되었다.
FastDelegate 이란 건데 같은 codeproject 사이트에서 Delegate로 검색해 보니 FastestDelegate로 알려질 만큼 유명한 글이었다. 자세한 설명은 링크와 소스 코드를 보는게 좋다. c++11 버전으로 내가 이해할 수 있게 다시 각색해 보았다. 다만, 원래 소스는 대부분의 c++ compiler를 지원하지만 내가 각색한 소스는 g++에서만 동작할 수도 있다. 특히, 원저자가 발명한(?) horrible_cast는 해커들이나 쓰는 방식이다. 적어도 Delegate에 관한 한 원저자가 주장하듯이 표준 c++을 따르는것 보다 portable code가 더 중요하다는 관점에 동의하지 않을 수 없다. 참고로, 표준 c++을 따르는 Delegate에 관한 글과 이를 modern c++로 구현한 글도 codeproject 사이트에 올라와 있다. 표준 방식의 문제는 과도한 template 사용으로 인해 사용자 인터페이스가 많이 불편하다는 것이다.
문제의 근원은 표준 c++ class의 member function pointer가 일반 pointer와는 달리 특정한 address를 갖지 못한다는데 있다. 구글링하다 보니 누군가 c++20에 넣어 달라는 글도 보이긴 하더라. 표준 c++에 Qt의 Signals & Slots를 넣어 달라는 요청도 번번히 거절 당해 왔는데 그 이유는 표준 방식으로도 Observer Pattern이나 Delegate Pattern을 사용해서 구현할 수 있다는 것이었다. 실상은 Signals & Slots와 같이 범용적으로 쓰기에는 제약사항이 많다.
여기서 Delegate는 Callback 함수와 동일한 개념이다. 표준 방식으로 std::function을 이용해서 구현할 수도 있는데 memory allocation이 사용되는 경우 느려질 수 있고, Signals & Slots와 같이 event callback에 사용하기에는 어려운 점이 많다. 참고로 Qt는 meta object compiler(moc)를 사용해서 Signals & Slot을 구현했다. Qt framework을 사용할 수 없는 경우에 표준 c++로 Signals & Slots를 구현하기란 쉽지 않은 일인 것이다.
아무튼, FastDelegate를 이용하면 Signals & Slots를 한결 수월하게 구현할 수 있을 듯 하다. 참고로, FastDelegate은 일반 함수(static function)와 class member 함수의 Callback을 모두 지원한다. 다만, 당시에는 lambda나 variadic template이 표준 c++이 아니었는데 variadic template 지원 부분은 추가 되었고, lamda의 경우엔 capture가 없으면 사용할 수 있다. 가령, 아래의 예와 같이 사용할 수 있다.
// lambda example : no captures only Delegate<double(int, double)> d1([](int i, double x) -> double { return x + i; }); Delegate<double(int, double)> d2, d3; auto lambda = [](int i, double x) -> double { return 2*x + i; }; d2 = lambda; d3 = [](int i, double x) -> double { return x*x + i; }; std::cout << d1(10, 3) << " " << d2(5, 6) << " " << d3(1, 1) << "\n";
// Rewrite FastDelegate.h by Don Clugston for g++ only(unportable). // Original FastDelegate is portable to almost all the compilers. // See http://www.codeproject.com/cpp/FastDelegate.asp for more information. namespace HIDDEN { class TGeneric; using TGenericP = TGeneric*; using TGenericPvMF = void(TGeneric::*)(); class Memento { public: Memento() = default; Memento(const Memento& rhs) : m_object(rhs.m_object), m_pmf(rhs.m_pmf) {} Memento& operator=(const Memento& rhs) { m_object = rhs.m_object; m_pmf = rhs.m_pmf; return *this; } bool operator!() const { return !m_object && !m_pmf; } bool operator==(const Memento &rhs) const { return m_object == rhs.m_object && m_pmf == rhs.m_pmf; } bool operator<(const Memento &rhs) const { if(m_object != rhs.m_object) return m_object < rhs.m_object; return std::memcmp(&m_pmf, &rhs.m_pmf, sizeof(m_pmf)) < 0; } bool operator>(const Memento& rhs) const { return rhs.operator<(*this); } size_t hash() const { return reinterpret_cast<size_t>(m_object) ^ unsafe_cast<size_t>(m_pmf); } protected: TGenericP m_object{nullptr}; TGenericPvMF m_pmf{nullptr}; private: template<class TO, class TI> static TO unsafe_cast(TI in) { union { TO out; TI in; } u; u.in = in; return u.out; } }; template<typename TGPMF, typename TPF> class Closure : public Memento { public: TGenericP object() const { return m_object; } TGPMF pmf() const { return reinterpret_cast<TGPMF>(m_pmf); } TPF function() const { return horrible_cast<TPF>(this); } template<class TB, class TPMF> void bind(const TB* pobj, TPMF pmf) { static_assert(sizeof(TGenericPvMF) == sizeof(pmf), "Unsupported conversion"); m_pmf = reinterpret_cast<TGenericPvMF>(pmf); m_object = reinterpret_cast<TGenericP>(const_cast<TB*>(pobj)); } template<class TB, class TPMF> void bind(TB* pobj, TPMF pmf, TPF pf) { if(!pf) { m_object = nullptr; m_pmf = nullptr; return; } bind(pobj, pmf); m_object = horrible_cast<TGenericP>(pf); } private: template<class TO, class TI> static TO horrible_cast(TI in) { union { TO out; TI in; } u; static_assert(sizeof(TI) == sizeof(u) && sizeof(TI) == sizeof(TO), "Unsupported conversion"); u.in = in; return u.out; } }; template<typename TR, typename... TArgs> class DelegateImpl { using TypePF = TR(*)(TArgs...); using TypePMF = TR(TGeneric::*)(TArgs...); public: DelegateImpl() = default; DelegateImpl(const DelegateImpl& rhs) : m_closure(rhs.m_closure) {} template <typename TB, typename TO> DelegateImpl(TO* pobj, TR(TB::*pmf)(TArgs... args)) { bind(pobj, pmf); } template <typename TB, typename TO> DelegateImpl(const TO* pobj, TR(TB::*pmf)(TArgs... args) const){ bind(pobj, pmf); } template<class TPF> DelegateImpl(TPF pf) { bind(pf); } void operator=(const DelegateImpl& rhs) { m_closure = rhs.m_closure; } template<class TPF> DelegateImpl& operator=(TPF pf) { bind(pf); } bool operator!() const { return !m_closure; } bool operator<(const DelegateImpl& rhs) const { return m_closure < rhs.m_closure; } bool operator>(const DelegateImpl& rhs) const { return !operator<(rhs); } bool operator==(const DelegateImpl& rhs) const { return m_closure == rhs.m_closure; } bool operator==(TypePF pf) const { return m_closure == pf; } bool operator!=(const DelegateImpl& rhs) const { return !operator==(rhs); } bool operator!=(TypePF pf) const { return !operator==(pf); } TR operator()(TArgs... args) const { return (m_closure.object()->*m_closure.pmf())(args...); } template <typename TB, typename TO> void bind(TO *pobj, TR(TB::*pmf)(TArgs... args)) { m_closure.bind(implicit_cast<TB*>(pobj), pmf); } template <typename TB, typename TO> void bind(const TO* pobj, TR(TB::*pmf)(TArgs... args) const) { m_closure.bind(implicit_cast<const TB*>(pobj), pmf); } template<class TPF> void bind(TPF pf) { m_closure.bind(this, &DelegateImpl::function, pf); } private: template <class TO, class TI> static TO implicit_cast(TI in) { return in; } TR function(TArgs... args) const { return (*m_closure.function())(args...); } Closure<TypePMF, TypePF> m_closure; }; } // namespace HIDDEN template<typename T> class Delegate; template<typename TR, typename... TArgs> class Delegate<TR(TArgs...)> : public HIDDEN::DelegateImpl<TR, TArgs...> { using TypeBase = HIDDEN::DelegateImpl<TR, TArgs...>; public: using TypeBase::TypeBase; Delegate() = default; template <typename TB, typename TO> Delegate(TO* pobj, TR(TB::*pmf)(TArgs... args)) : TypeBase(pobj, pmf) {} template <typename TB, typename TO> Delegate(const TO* pobj, TR(TB::*pmf)(TArgs... args) const) : TypeBase(pobj, pmf) {} Delegate(TR(*pf)(TArgs... args)) : TypeBase(pf) {} }; template <typename TR, typename... TArgs> Delegate<TR(TArgs...)> makeDelegate(TR(*pf)(TArgs...)) { return Delegate<TR(TArgs...)>(pf); } template <typename TR, typename TB, typename TO, typename... TArgs> Delegate<TR(TArgs...)> makeDelegate(TO* pobj, TR(TB::*pmf)(TArgs...)) { return Delegate<TR(TArgs...)>(pobj, pmf); } template <typename TR, typename TB, typename TO, typename... TArgs> Delegate<TR(TArgs...)> makeDelegate(TO* pobj, TR(TB::*pmf)(TArgs...) const) { return Delegate<TR(TArgs...)>(pobj, pmf); }
댓글 없음:
댓글 쓰기