Nov 3, 2024

[C++] type erasure sum-up

Reference:
C++ Type Erasure Demystified - Fedor G Pikus - C++Now 2024

  • Type erasure as a design pattern.
  • Introduces indirections, performance hit.

Purpose
Make type generic.

e.g.

Definition

Type erasure is an abstraction for multiple implementations that provide the same behavior(the relevant behavior is what matters)

Problem it solves
  • The code expects certain behavior
  • The code is written in terms of an abstraction that provides this behavior.
  • Many concrete types can implement this behavior.
  • All properties of these types that are no relevant to the behavior are erased.
  • Type erasure separates the interface from the implementation.
  • So does inheritance, but type erasure does not require common base class.
  • Type erasure is non-intrusive
  • External polymorphism(types do not have to be designed for it)
  • Avoid recompile.
Type erasure for decoupling dependencies; e.g.

class Network {
	void send(const char* data);
	void receive(const char* buffer);
};

// flexible; Strategy pattern:
class Network {
 std::function<const char*(const char*)? processor = [](const char*c){return c};

 void send(const char* data) {
  data = processor(data);
 }

 template<typename F> void SetProcessor(F&& f) {
   processor = std::forward<F>(f);
 }
};

Three ways to implement type erasure in C++

  1. Inheritance
  2. static functions
  3. v_table

1)
template<typename T> class SmartPtr {
 struct destroy_base {
   virtual void operator()(T*) = 0;
   virtual ~destroy_base() {}
 };

 template<typename Deleter>
  struct destroy : destroy_base {
    destroy (Deleter d) : d_(d) {}
    void operator()(T* p) override { d_(p); }
      Deleter d_;
    };
	
    // local buffer on stack instead on heap.
    alignas(8) char buf_[16];
    destroy_base* d_ = new(buf_) destroy<std::default_delete<T>>(std::default_delete<T>()};
    T* p_;

  public:
    template<typename Deleter> SmartPtr (T* p, Deleter d) : p_(p), d_(new destroy<Deleter<d(d)) {}

    ~SmartPtr() { (*d_)(p_); delete d_; }
};
  • Slow due to calling new destroy<> every time. 
  • Local buffer helps. 
  • If local buffer is too small, switch to heap.
    std::function does above. 
  • static_assert in the compiler.
    high performance computing does above. 
  • Make the buff size a template parameter of the class. 

2)
template<typename T> void reify_func(void* p) {
  T* q = static_cast<T*>(p); ...
}

void(*)(void*) fp = reify_func<MyDeleter>;

template<typename T> class SmartPtr {
 void(*)(T*, void*) destroy_;
 template<typename Deleter>
   static void invoke_destroy(T* p, void* d) {
     static_cast<Deleter*>(d)->(p);
    }

 void* d_;
 T* p_;

 public:
  template<typename Deleter> SmartPtr (T* p, Deleter* d) : 
    destroy_(invoke_destroy<Deleter>), d_(d),  p_(p) {}

  ~SmartPtr() { destroy_(p_, d_); }
};

3) 

template<typename T> class SmartPtr {
 struct vtable_t {
   using destroy_t = void(*)(T*, void*);
   using destructor_t = void(*)(void*);
   destroy_t destroy_;
   destructor_t destructor_;
 };

 template<typename Deleter>
 static void destroy(T* p, void* d) {
  static_cast<Deleter*>(d)->(p);
 }

 template<typename Deleter>
 static void destructor(void* d) {
  static_cast<Deleter*>(d)->~Deleter();
 }

 template<typename Deleter>
 constexpr static vtable_t vtable = {
   SmartPtr::template destroy<Deleter>,
   SmartPtr::template destructor<Deleter>,
 };

 alignas(8) char buf_[8];
 T* p_;
 const vtable_t* vtable_ = nullptr;

 public:
 template<typename Deleter>
 SmartPtr(T* p, Deleter d) : p_(p), vtable_(&vtable<Deleter>){
  static_assert(sizeof(Deleter) <= sizeof(buf_);
  ::new (static_cast<void*>(buf_)) Deleter(d);
 }

 ~SmartPtr() {
  vtable_->destroy_(p_, buf_);
  vtable_->destructor_(buf_);
 }
};

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.