2018/09/18

Signals & Slots - dangling pointer 예방


이전 글에 이어서 여기서는 Signals & Slot에서 dangling pointer가 발생하지 않도록 하기 위한 방법을 구현해 보았다. 참고로, 객체 관점에서 보면 Signals & Slots란 용어 때문에 각각의 객체가 분리된 듯 느껴지는데 실상은 한 객체가 여러 개의 Signal과 여러 개의 Slot을 동시에 가질 수도 있다. 하나의 Signal에 여러 개의 Slot이 연결될 수 있고, 또한 특정 Slot이 여러 개의 Signal에 연결될 수도 있다.

기본적인 발상은, 특정 Signal에 Slot 들을 연결할 때 각각의 Slot 쪽에도 해당 Signal pointer를 저장하는 것이다. 그리고 나서 특정 Slot 객체 소멸시에 Signal 객체 내의 해당 Slot pointer를 삭제해 주면 된다. 그런데, Slot 쪽에 Signal 정보를 저장하면서 파생되는 문제가 있는데, 이제는 Signal 객체가 Slot 객체보다 먼저 소멸시에 Slot 쪽에 좀비 Signal pointer가 발생하게 된다. 즉, Signal 객체 소멸시에 연결된 Slot들에 저장된 Signal pointer도 삭제해야 한다.

이렇듯 Observer Pattern에서는 상호참조(circular reference) 문제가 발생하게 된다. Smart pointer 사용시에는 shared pointer만 사용시 서로 counter를 물고 있어서 pointer 삭제가 안되므로 한쪽은 weak pointer를 사용하여 문제를 해결한다. 하지만, 여기서는 raw pointer를 사용해야만 하기 때문에 Signal이든 Slot이든 객체 소멸시에 참조 pointer들을 삭제해 주면 된다.

Tracker class

Slot 객체에 Signal pointer를 저장하려면 저장소를 달아 주어야 하는데 기존의 class가 아래 소개한 Tracker class를 상속받으면 된다. Qt에서는 시조 할배 class인 QObject란 놈이 있는데 이런 시조 할배 류의 class에 매달아 놓으면, 대대 손손 모든 객체의 Signals & Slots 좀비 pointer들을 예방할 수 있게 된다. QObject는 사실 Tracker의 역할을 포함하고 있다.

참고로, Qt에서 Signals & Slots를 사용하려면 class 마다 QObject class를 상속하고 Q_OBJECT macro를 선언해 주어야 한다. macro는 moc(meta object compiler)에서 사용할 meta 정보에 대한 class member 변수와 함수들을 추가하기 위한 것이다. moc가 build시에 Signals와 Slots간의 연결 meta 정보를 생성한 후, 나중에 link하여 binary 파일을 생성하게 해준다. 덕분에 container를 사용하지 않음으로써 container 내부적으로 memory allocation/deallocation 시 발생하는 system call 성능 부담이 줄어든단다.

아무튼, Tracker를 상속받은 class들 간에 다중 상속을 한다면 그 class 들은 virtual public Tracker로 상속해야 한다. QObject의 경우도 마찬가지인데, 다중 상속시에 Diamond problem이 생기기 때문이다. 대부분의 경우, 다중 상속을 피하려 하기 때문에 굳이 virtual로 상속할 일은 거의 없을 것이다.

그리고, Tracker를 상속하지 않은 class 객체를 memory allocation해서 Slot으로 사용하면, 그 객체가 Signal 객체보다 먼저 소멸한 후 Signal을 발생시키면 crash가 날 것이다. 이것이 Tracker가 필요한 이유이다. Tracker의 virtual destructor가 dangling pointer들을 자동 삭제하도록 했기 때문에, 성능이 문제가 된다면 Tracker의 virtual destructor를 삭제하고 Slot 객체 소멸시 Tracker의 destroyTracker()를 Slot의 destroyed와 같은 Signal에 미리 connect해 주는 것도 한가지 방법이 될 수 있겠다.

그리고, shared_ptr를 사용하여 Slot을 Signal에 연결할 경우에는 Signals & Slots의 구조적인 문제때문에 weak_ptr를 사용할 수 없는데 weak_ptr를 사용하지 않고도 Slot 객체 소멸시 dangling pointer를 제거할 수 있는 방법을 시험해 보고 있는데 잘 된다. 그니까, Slot 연결시 shared_ptr를 사용하면 Tracker를 사용할 필요가 없겠다. 하지만, shared_ptr를 쓰지 않는 경우도 있으니까, Tracker와 같이 사용하는 것이 좋겠다. 일종의 garbage collector 역할이다.

Signal::connect()

이 Tracker의 저장소(m_tracker)는 다름아닌 Signal 객체이다. 특정 Signal에 Slot 객체를 연결하면 Signal에는 Slot 객체와 Slot으로 사용할 member function에 대한 pointer가 저장된다. 특정 Signal 객체에 Slot 객체를 연결(connect)시, Tracker가 달린 Slot 객체에 연결중인 Signal 객체와 이 놈이 이 놈에 연결된 Slot(Tracker)을  제거(disconnect)할 함수 정보를 저장했다가, Slot(Tracker) 객체 소멸시 연결된 모든 Signal 내의 Slot(Tracker) 정보를 삭제하도록 하면 되는 것이다. Tracker 입장에서 보면  Tracker 자신이 Signal이 되고, 여기에는 연결된 Signal과 그 Signal이 Tracker를 삭제하기 위한 방법이 곧 Slot 객체로써 저장된 셈이다. 그니까 원래 Signals와 Slots 객체를 구분하는 것이 아무 의미가 없다.

Qt에서는 객체 소멸시에 destroyed 란 Signal을 발생시킨다(emit). 나중에 객체 정리 작업에 필요한 Slot과 연결해서 사용할 수 있도록 하기 위한 것이다. 여기서 구현한 방법은 Tracker내의 m_tracker Signal이 바로 destroyed란 Signal에 해당하는 것이고, Slot에 해당하는 역할은, 단순히 연결된 Signal 들에 저장된 Tracker pointer 자신을 자동으로 제거하게 하는 것이다. Signal class의 connect() 함수 부분은 Tracker가 Slot 객체 class에 달려 있는지 compile 시에 check해서 Tracker 객체가 생성될 때만 Signal 정보를 Tracker에 저장하도록 한다.

Signal::disconnect()

 Signal 객체 소멸시에는 disconnect() 함수를 자동 호출해서 연결된 모든 Slot 객체를 제거하게 되는데, 그 전에 Tracker 객체가 존재하는 Slot에 대해서는 Tracker에 이미 저장된 Signal과 그 제거 방법을 호출하도록 해주면 된다. 문제는 Signal에 Slot 객체 pointer는 갖고 있지만 Tracker pointer는 갖고 있지 않다는 것이다. Slot이 Tracker를 상속받고 있으니까 reinterpret_cast든  c-style cast든 Slot 객체를 Tracker 객체로 casting 해주면 대개의 경우 문제가 없는데, 다중 상속인 경우 문제가 되더라. 특히 virtual 상속을 받는 경우 Tracker 주소를 못찾는다. 그래서 어쩔 수 없이 Slot의 Store class에 Tracker에 대한 pointer(m_ptr)를 저장하도록 함으로써 문제를 해결했다.

맺음말

설명과 소스까지 분리되어 있어서 내가 읽어도 뭔 소린지 모르겠네...

근본적으로는 표준 c++의 member function pointer를 일반 pointer 처럼 쓸수 있다면 좀더 쉽게 문제를 해결할 수 있겠지만 현실적으로는 불가능하다. 다만, 여기서는 gcc/g++ 처럼 고정 크기 member function pointer를 사용하는 compiler라면 상대적으로 쉽게 Signals & Slots를 구현할 수 있음을 알아낸 것으로 만족해야겠다. macro나 moc 같은 compiler가 없이 순수 c++ 언어로 Signals & Slots를 구현할 수 있지 않을까 하는 호기심이 풀렸다.

Boost Signals2 소스를 내려받아서 비교해 보려고 했는데 소스량도 많지만 한마디로 난독 코드다. Template만으로도 부족해서 macro language 그 자체다. Qt도 뭐 macro를 과도하게 쓰는 편이었는데 c++11 이후 macro 대신 c++ 언어 표준에 충실해 지려고 노력하는 티가 보인다.

아무튼 c++ 표준 방식은 아니지만 Signals & Slots에서 dangling pointer가 발생하지 않도록 하기 위한 방법까지 구현해 보았다. 여기서 참고할 점은 Tracker class는 일종의 plugin 처럼 동작한다. 그니까 Slot class가 Tracker class를 상속하든 안하든 전체 Signals & Slots는 정상 동작한다. Tracker plugin을 자동으로 탐지하고, 탐지가 되면 자동으로 객체 소멸시에 Signal 객체든 Slot 객체든 불필요한 좀비 pointer가 남지 않도록 해 준다.

Signals & Slot 관점에서 추가적으로 필요한 기능들이 많지만 이만 접겠다.
  • muti-thread 지원
  • Slot이 여러 개일 경우 return 값들에 대한 handler 지원
  • event queue 연계 - async Signals 지원
다만, 현재 구현된 것만도 쓰일 곳이 있으니까 사용법에 대해서도 나중에 정리할 예정이다. 사실, 여기 구현된 Tracker 자동화에 이미 Signals & Slots가 활용되고 있으니까 소스를 잘 쳐다보면 어떻게 사용하는지도 알 수 있긴 하겠다.


// 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 GPL version 3, as detailed in
// https://opensource.org/licenses/gpl-3.0.html .

// 추가된 Class
class Tracker;

class Store
{
  ......
protected:
  // 추가된 member 변수
  Tracker* m_ptr {nullptr};     // pointer to a Tracker for this Slot
};

template<typename TR, typename... TArgs>
class Slot : public Store
{
  ......
  // 추가된 함수들
  Tracker* ptracker() const { return m_ptr; }
  void setPTracker(Tracker* ptr) { m_ptr = ptr; }
  ......
};

template<typename T> class Signal;
template<typename TR, typename... TArgs>
class Signal<TR(TArgs...)>
{
  ......
  // 수정된 함수들
  template<typename TB, typename TO>
  void connect(TO* pobj, TR(TB::*pmf)(TArgs...));

  template<typename TB, typename TO>
  void connect(const TO* pobj, TR(TB::*pmf)(TArgs...) const);
  ......
  void disconnect();
  ......
  //추가된 함수
  void disconnect(TGenC* pobj)
  {
    if(!pobj) return;
    for(auto it = m_slots.cbegin(); it != m_slots.cend();) {
      if(it->object() == pobj || reinterpret_cast<TGenC*>(it->ptracker()) == pobj)
        it = m_slots.erase(it);
      else ++it;
    }
  }
  ......
};

// dangling pointer 예방을 위한 Slot plugin class - Slot에 대한 base class로 사용
class Tracker
{
public:
  Tracker() = default;
  Tracker(const Tracker&) = default;
  Tracker& operator=(const Tracker&) = default;
  Tracker(Tracker&&) = default;
  Tracker& operator=(Tracker&&) = default;

  virtual ~Tracker() { destroyTracker(); }

  Signal<void(TGenC*)>& tracker() { return m_tracker; }

  void destroyTracker() { m_tracker.emit(reinterpret_cast<TGenC*>(this)); }

private:
  Signal<void(TGenC*)> m_tracker;
};

template<typename TR, typename... TArgs>
void Signal<TR(TArgs...)>::disconnect()
{
  while(m_slots.size()) {
    if(Tracker *ptr = m_slots.back().ptracker()) {
      Signal<void(TGenC*)>& (Tracker::*ptmf)() = &Tracker::tracker;
      (ptr->*ptmf)().disconnect(reinterpret_cast<TGenC*>(this));
    }
    m_slots.pop_back();
  }
}

template<typename TR, typename... TArgs>
template<typename TB, typename TO>
void Signal<TR(TArgs...)>::connect(TO* pobj, TR(TB::*pmf)(TArgs...))
{
  if(!pobj) return;
  TypeSlot tmp(implicit_cast<const TB*>(pobj), pmf);
  if(contains(tmp)) return;
  m_slots.push_back(std::move(tmp));

  if(std::is_convertible<decltype(const_cast<TO*>(pobj)), Tracker*>::value) {
    Tracker* ptr = (Tracker*)(pobj); // only effective casting for multiple inheritances.
    m_slots.back().setPTracker(ptr);
    Signal<void(TGenC*)>& (Tracker::*ptmf)() = &Tracker::tracker;
    // static_cast<> is required for template type deduction of overloaded functions.
    (ptr->*ptmf)().connect(this, static_cast<TR(Signal<TR(TArgs...)>::*)(TGenC*)>
                                 (&Signal<TR(TArgs...)>::disconnect));
  }
}

template<typename TR, typename... TArgs>
template<typename TB, typename TO>
void Signal<TR(TArgs...)>::connect(const TO* pobj, TR(TB::*pmf)(TArgs...) const)
{
  if(!pobj) return;
  TypeSlot tmp(implicit_cast<const TB*>(pobj), pmf);
  if(contains(tmp)) return;
  m_slots.push_back(std::move(tmp));

  if(std::is_convertible<decltype(const_cast<TO*>(pobj)), Tracker*>::value) {
    Tracker* ptr = (Tracker*)(const_cast<TO*>(pobj)); // only effective casting...
    m_slots.back().setPTracker(ptr);
    Signal<void(TGenC*)>& (Tracker::*ptmf)() = &Tracker::tracker;
    // static_cast<> is required for template type deduction of overloaded functions.
    (ptr->*ptmf)().connect(this, static_cast<TR(Signal<TR(TArgs...)>::*)(TGenC*)>
                                 (&Signal<TR(TArgs...)>::disconnect));
  }
}

댓글 없음:

댓글 쓰기