이전 글의 Delegate 개념을 이용해서 Signals and Slots를 구현해 보았다. 구글링해 보면 Qt의 Signals and Slots를 구현해 보려고 했던 과거의 여러 시도들을 찾을 수 있다. Libsigc++이 대표적인 사례이다. 물론, Boost signals2/signals도 있다. 잘 굴러가는 바퀴들이 많은 데 잘 굴러가지 않을 가능성이 높은 바퀴를 또 만들고 싶은 것은 단순한 호기심 때문이다. 그 호기심의 근원은 저렇게 복잡하게 별의 별짓을 다해 가면서 구현한 방법들이 맘에 들지 않았고, 더 쉬운 방법이 있지 않을까 하는 것이다.
c++에서 가능한 모든 종류의 함수 객체(function object)들을 저장했다가 원하는 시점에 한꺼번에 호출할 수 있는 방법이 Signals and Slots이다. 근본적으로는 class 간의 coupling 문제를 해결해 주기 때문에 program 설계와 재사용 측면에서 매우 중요한 개념이기도 하다. 여기서, 함수 객체는 일반 static/global 함수와 class member 함수 뿐만 아니라, lambda(capture 포함), std::function이나 사용자가 만든 functor 등의 모든 호출 가능한 객체들을 의미한다.
앞서 Delegate에 대한 글에서 얘기했듯이 표준 c++에서 member function에 대한 pointer 크기가 가변적이므로 Delegate나 Signal & Slots를 표준 c++에서 구현하기가 매우 어렵다. c++ 언어의 장점이 막강한 유연성을 기반으로 구현 못할 것이 없는 언어인데 member function pointer에 대한 제약이 있다는 것은 굉장히 놀라운 사실이다. 물론, modern c++가 추구하는 바는 가능한 raw pointer를 사용하지 못하게 하는 것이긴 하지만, 언어 자체에 제약사항이 있는 것은 바람직하지 않다고 본다. 더구나 pointer가 없는 c/c++는 앙꼬빠진 찐빵이다. 추측컨대 MS를 비롯한 상용 컴파일러 Vendor들이 자신들의 컴파일러가 갖고 있는 제약사항이기 때문에 표준으로 채택하지 않았을 가능성이 매무 높다고 본다. 왜냐하면 gcc/g++나 clang에서는 표준이 아님에도 이미 고정 크기 member function pointer를 사용하기 때문이다. 이것이 중요한 이유는 모든 함수 객체를 쉽게 저장했다가 나중에 호출할 수 있기 때문이다.
이 글은 표준 c++ 방식은 아니지만 member function pointer를 저장하는 것이 Signals and Slots를 구현하는데 얼마나 편리한지를 보여주는 구체적인 예가 될 것이다. 컴파일 오류가 나면 컴파일러가 지원하지 않는다는 것을 알 수 있다.
모든 종류의 함수 객체를 저장하는 데는, 객체 자신에 대한 pointer와 class member function의 경우 이에 대한 pointer를 포함해서, 두 개의 저장소면 충분하다. 가령, static/global 함수는 객체 pointer에 저장할 수 있다. std::function이나 functor, lambda는 모두 class object와 동일하다.
c++은 strictly typed language이기 때문에 저장했던 pointer들을 원래의 type으로 변환해야 함수 객체들을 실행할 수 있는데, 같은 type에 대한 pointer의 크기가 동일하다면 generic class 객체의 pointer를 이용해서 원래의 함수 객체들을 호출할 수 있다. 이것이 reinterpret_cast를 사용하는 이유이고 또한 이를 사용할 경우 컴파일러에 대한 portability가 낮아질 수 밖에 없는 이유이기도 하다.
Signal 객체 하나는 동일한 arguments와 return type을 갖는 서로 다른 종류의 Slot 함수 객체들을 std::vector container에 저장한다. 얼핏 생각하면 서로 다른 종류의 함수 객체들을 std::vector에 저장하려면 c++17의 std::any 객체를 사용해야 하는 것이 아닌가 할 수도 있지만, template type으로는 모두 동일한 type이 되므로 std::any객체를 사용할 필요는 없다. std::set이나 std::unordered_set이 container로써 적합해 보이지만, Signal에 connect되는 순서에 따라 Slot 객체 들의 실행 순서가 결정되기 때문에 vector를 사용하되, 이미 container에 있는 객체는 다시 connect해도 중복으로 container에 저장되지 않도록 했다.
아래 소스에는 dangling pointer에 대해 고려하지 않았다. Signals & Slots도 Observer Pattern의 일종이라 생각할 수도 있는데, Signal에 연결된 Slot 객체가 Signal 보다 먼저 소멸했을때 Signal에 저장된 Slot pointer 들이 좀비가 되어 문제가 생길 수 있다. multi-thread를 사용하지 않는 한 크게 문제되지는 않겠지만, 여기서는 smart pointer를 사용하지 않는 방식이기 때문에 나중에 고려해 볼 예정이다.
아무튼 여기서 소개한 Signals & Slots의 장점은, code가 직관적이고, Slot 객체에 대해 복사 또는 복제하는 방식이 아니고 메모리를 직접 참조하므로 성능이 좋을 가능성이 높다. 단점은 c++ 표준 방식이 아니므로 compiler 호환성 또는 이식성이 떨어질 수 밖에 없다.
// Signals & Slots C++ Implementation // // This program is copyright (c) 2018 by Umundu @ https://zapary.blogspot.com . // It is distributed under the terms of the GNU LGPL version 3, as detailed in // https://opensource.org/licenses/lgpl-3.0.html . #include <vector> class TGenC; class Store { public: bool operator!() const { return !m_obj && !m_pmf; } bool operator==(const Store& rhs) const { return m_obj == rhs.m_obj && m_pmf == rhs.m_pmf; } protected: using TGvPMF = void(TGenC::*)(); TGenC* m_obj {nullptr}; // pointer to an object or a function TGvPMF m_pmf {nullptr}; // pointer to a member function }; template <typename TR, typename... TArgs> class Slot : public Store { private: using TypePF = TR(*)(TArgs...); using TypePMF = TR(TGenC::*)(TArgs...); template<typename TF> using TypeIfRValue = typename std::enable_if<!std::is_reference<TF>{}>::type; public: Slot(TypePF pf) { bind(pf); } template <typename TB, typename TPMF> Slot(const TB* pobj, TPMF pmf) { bind(pobj, pmf); } template<typename TB, typename TPMF, typename = TypeIfRValue<TB>> Slot(TB&& pobj, TPMF pmf) { bind(std::forward<TB>(pobj), pmf); } TGenC* object() const { return m_obj; } TypePMF pmf() const { return reinterpret_cast<TypePMF>(m_pmf); } TR operator()(TArgs... args) const { emit(args...); } TR emit(TArgs... args) const { if(!m_obj) return; if(m_pmf) return (m_obj->*(pmf()))(args...); else return (*reinterpret_cast<TypePF>(m_obj))(args...); } void bind(TypePF pf) { static_assert(sizeof(TGenC*) == sizeof(pf), "Compiler unsupported."); m_obj = reinterpret_cast<TGenC*>(pf); } template <typename TB, typename TPMF> void bind(const TB* pobj, TPMF pmf) { static_assert(sizeof(TGvPMF) == sizeof(pmf), "Compiler Unsupported."); m_pmf = reinterpret_cast<TGvPMF>(pmf); m_obj = reinterpret_cast<TGenC*>(const_cast<TB*>(pobj)); } template<typename TB, typename TPMF> TypeIfRValue<TB> bind(TB&& pobj, TPMF pmf) { static_assert(sizeof(TGvPMF) == sizeof(pmf), "Compiler Unsupported."); m_pmf = reinterpret_cast<TGvPMF>(pmf); m_obj = reinterpret_cast<TGenC*>(&pobj); } }; template <typename T> class Signal; template <typename TR, typename... TArgs> class Signal<TR(TArgs...)> { private: using TypePF = TR(*)(TArgs...); using TypeSlot = Slot<TR, TArgs...>; template<typename TF> using TypeIfRValue = typename std::enable_if<!std::is_reference<TF>{}>::type; public: Signal() = default; Signal(TypePF pf) { if(!*this) connect(pf); } template <typename TF> Signal(TF* pobj) { if(!*this) connect(pobj); } template <typename TB, typename TO> Signal(TO* pobj, TR(TB::*pmf)(TArgs...)) { if(!*this) connect(pobj, pmf); } template <typename TB, typename TO> Signal(const TO* pobj, TR(TB::*pmf)(TArgs...) const) { if(!*this) connect(pobj, pmf); } ~Signal() { disconnect(); } template <typename TPF> Signal& operator=(TPF pf) { if(!*this) connect(pf); } template <typename TF> Signal& operator=(TF* pobj) { if(!*this) connect(pobj); } const TypeSlot& operator[](int i) const { return m_slots.at(i); } bool operator!() const { return m_slots.empty(); } bool operator==(const Signal& rhs) const { return m_slots == rhs.m_slots; } bool operator!=(const Signal& rhs) const { return !operator==(rhs); } void operator()(TArgs... args) const { emit(args...); } void emit(TArgs... args) const { for(auto it = m_slots.cbegin(); it != m_slots.cend(); ++it) (*it)(args...); } int size() const { return m_slots.size(); } bool contains(TypeSlot& slot) { for(auto it = m_slots.cbegin(); it != m_slots.cend(); ++it) if(*it == slot) return true; return false; } void connect(TypePF pf) { if(!pf) return; TypeSlot tmp(pf); if(!contains(tmp)) m_slots.push_back(std::move(tmp)); } template <typename TB, typename TO> void connect(TO* pobj, TR(TB::*pmf)(TArgs...)) { if(!pobj) return; TypeSlot tmp(implicit_cast<const TB*>(pobj), pmf); if(!contains(tmp)) m_slots.push_back(std::move(tmp)); } template <typename TB, typename TO> void connect(const TO* pobj, TR(TB::*pmf)(TArgs...) const) { if(!pobj) return; TypeSlot tmp(implicit_cast<const TB*>(pobj), pmf); if(!contains(tmp)) m_slots.push_back(std::move(tmp)); } template <typename TF> void connect(TF* pobj) { if(!pobj) return; TypeSlot tmp(pobj, &TF::operator()); if(!contains(tmp)) m_slots.push_back(std::move(tmp)); } template<typename TF> TypeIfRValue<TF> connect(TF&& pobj) { TypeSlot tmp(std::forward<TF>(pobj), &TF::operator()); if(!contains(tmp)) m_slots.push_back(std::move(tmp)); } template <typename TB, typename TO> void disconnect(const TO* pobj, TR(TB::*pmf)(TArgs...)) { if(!pobj) return; TypeSlot tmp(pobj, pmf); for(auto it = m_slots.cbegin(); it != m_slots.cend(); ++it) { if(*it == tmp) { m_slots.erase(it); return; } } } template <typename TB, typename TO> void disconnect(const TO* pobj, TR(TB::*pmf)(TArgs...) const) { if(!pobj) return; TypeSlot tmp(implicit_cast<const TB*>(pobj), pmf); for(auto it = m_slots.cbegin(); it != m_slots.cend(); ++it) { if(*it == tmp) { m_slots.erase(it); return; } } } template <typename TO> void disconnect(const TO* pobj) { if(!pobj) return; TGenC* tmp = reinterpret_cast<TGenC*>(const_cast<TO*>(pobj)); for(auto it = m_slots.cbegin(); it != m_slots.cend();) { if(it->object() == tmp) it = m_slots.erase(it); else ++it; } } void disconnect(TypePF pf) { if(!pf) return; TypeSlot tmp(pf); for(auto it = m_slots.cbegin(); it != m_slots.cend(); ++it) { if(*it == tmp) { m_slots.erase(it); return; } } } void disconnect() { m_slots.clear(); } private: template <typename TO, typename TI> TO implicit_cast(TI in) { return in; } std::vector<TypeSlot> m_slots; };
댓글 없음:
댓글 쓰기