2020/09/14

c++ Type Erasure 활용

 

이전 글에서 만든 표준 c++ 기반의 Slot Delegate를 이용해서 예전에 만들었던 Signal 부분도 표준 c++로 바꾸려다 보니, 또 다른 종류의 Type Erasure를 사용하면 쉽게 해결되는 부분을 발견하였다. 그것은 Slot이 shared_ptr<>인 경우 Signal container에서 자동으로 life-cycle을 tracking 해서 dangling pointer를 없애주는 부분이다.

이 놈도 Delegate 만큼이나 흥미로운 놈이다. c++17의 std::any와 비슷하지만 사용법이 다르다. 옛날 옛적엔 서로 다른 type의 객체를 컨테이너에 가두고 뺑뺑이 돌리기 위해 void*를 사용했었는데, 이제는 이 놈을 사용하는게 좋겠다. 아래에 std::shared_ptr<>를 가지고 AnySharedPtr 라는 Type Erasure를 구현한 예를 보였다. 사용하기는 편하지만, 이 놈의 단점은 객체를 저장하기 위해 memory allocation이 필요하고 이에 따라 성능에도 악영향을 준다. 아무튼 Signal에서 shared_ptr<>가 reset 되거나 scope을 벗어나는 경우, p.get()과 p.use_count() 정보 만으로도 해당 Slot을 자동 제거해 줄 수 있다. 

이 놈이 재미있는 것은 Delegate 처럼 상속을 사용하지 않는다(내부에서만 사용함). std::function<> 류의 Delegate은 함수형의 any callables를 대신하기 위해 사용되는데 비해, 이 놈은 std::any와 같이 any 객체에 사용될 수 있다. 하지만, std::any와는 달리 std::any_cast<>를 사용할 필요가 없다. 돌려서 말하면, Delegate와 마찬가지로 type이 지워져서 객체 type을 정확히 알아야 하는 경우엔 사용할 수가 없다. 

또한, Visitor Pattern을 사용하지 않고도 공통 interface만 있으면 서로 다른 유형의 객체를 std::vector<>와 같은 컨테이너에 담아서 반복 작업을 돌릴 수 있다. 아래의 예를 보는게 이해가 빠를 것이다.


#include <memory>

// Type Erasure for any type of std::shared_ptr<>s.
//
// - typical usage: save any types to a container and iterate some common works.
//   std::vector<AnysharedPtr> shared_pointers;

class AnySharedPtr
{
  class Concept
  {
  public:
    auto clone() const { return std::unique_ptr<Concept>(clone_impl()); }

    // Interface methods for Any types. (e.g. std::shared_ptr<> here)
    // - the return type is limited to basic POD types: the original type is erased.
    virtual void* get() const = 0; // NB: the original pointer type is erased.
    virtual long use_count() const = 0;

  protected:
    // Virtual constructor idiom
    virtual Concept* clone_impl() const = 0; // virtual copy constructor - base return
  };

  template <typename TM>
  class Model : public Concept
  {
  public:
    Model(TM&& obj) : m_model(std::forward<TM>(obj)) {}

  protected:
    // Virtual constructor idiom
    Model* clone_impl() const override { return new Model(*this); } // covariant return

    // Interface methods for Any types. (e.g. std::shared_ptr<> here)
    void* get() const override { return m_model.get(); }
    long use_count() const override { return m_model.use_count(); }

  private:
    TM m_model;
  };

public:
  // The rule of five is applied to use std::unique_ptr<> as a member variable.
  AnySharedPtr() = default;
  AnySharedPtr(const AnySharedPtr& rhs) : m_concept(rhs.m_concept->clone()) {}
  // Non-const constructor is required here to prevent the perfect forwarding constuctor.
  AnySharedPtr(AnySharedPtr &rhs) : AnySharedPtr(static_cast<const AnySharedPtr&>(rhs)) {}
  AnySharedPtr(AnySharedPtr&&) = default;
  template <typename TM> // perfect forwarding constructor
  AnySharedPtr(TM&& obj) : m_concept(std::make_unique<Model<TM>>(std::forward<TM>(obj))) {}
  ~AnySharedPtr() = default;

  AnySharedPtr& operator=(const AnySharedPtr& rhs)
  { m_concept = rhs.m_concept->clone(); return *this; }
  AnySharedPtr& operator=(AnySharedPtr& rhs)
  { return operator=(static_cast<const AnySharedPtr&>(rhs)); }
  AnySharedPtr& operator=(AnySharedPtr&&) = default;
  template <typename TM> // perfect forwarding assignment operator
  AnySharedPtr& operator=(TM&& obj)
  { m_concept = std::make_unique<Model<TM>>(std::forward<TM>(obj)); return *this; }

  // Interface methods for Any types. (e.g. std::shared_ptr<> here)
  void* get() const { return m_concept->get(); }
  long use_count() const { return m_concept->use_count(); }

private:
  std::unique_ptr<Concept> m_concept; // storage for type erasure
};

int main()
{
  std::shared_ptr<int> si = std::make_shared<int>(10), si_copy1 = si, si_copy2 = si_copy1;
  std::shared_ptr<std::string> ss = std::make_shared<std::string>("hello"), ss_copy = ss;
  
  std::vector<AnySharedPtr> any_ptrs;
  any_ptrs.push_back(si); any_ptrs.push_back(ss);
  for(auto p : any_ptrs) { std::cout << "Ptr: " << p.get() << ", " << p.use_count() << '\n'; }
  
  return 0;
}