2

shared::ptr implementations that i found are written in this way

namespace detail {
   struct deleter_base {
      virtual ~deleter_base() {}
      virtual void operator()( void* ) = 0;
   };
   template <typename T>
   struct deleter : deleter_base {
      virtual void operator()( void* p ) {
         delete static_cast<T*>(p);
      }
   };
}
template <typename T>
class simple_ptr {
   T* ptr;
   detail::deleter_base* deleter;
public:
   template <typename U>
   simple_ptr( U* p ) {
      ptr = p;
      deleter = new detail::deleter<U>();
   }
   ~simple_ptr() {
      (*deleter)( ptr );
      delete deleter;
   }
};

My questions are

1) Why do we need to have structures like this (i mean something like type erasure technique), can't we just have this as deleter(see code below)? What is the purpose of having a virtual function here, as I understand if it isn't virtual it will anyway call delete on the correct type (i.e. on Bar type here std::shared_ptr<Foo>(new Bar)), because simple_ptr has templated constructor.

template <typename T>
   struct deleter {
      void operator()( void* p ) {
         delete static_cast<T*>(p);
      }
   };

2) Why we need a virtual destructor in the base class? Is it only because it contains a virtual function or our deleter must have virtual destructor too?

alter_igel
  • 6,899
  • 3
  • 21
  • 40
Karen Melikyan
  • 195
  • 1
  • 9
  • Your second question is answered here: https://stackoverflow.com/questions/461203/when-to-use-virtual-destructors – alter_igel Oct 23 '18 at 18:48
  • Your example would work for trivial deleters that need only a single function to work, but if you want a deleter with some kind of state or persistent data, then your example wouldn't work, while the polymorphic option would. – alter_igel Oct 23 '18 at 18:50
  • Your `deleter` cannot be stored in `simple_ptr` as there is no type (barring `void*`) that can store `deleter` for any `T`. And in a `void*`, it cannot be invoked. So I'm not sure what you are asking. – Yakk - Adam Nevraumont Oct 23 '18 at 19:04
  • @Yakk-AdamNevraumont ah, thank you a lot, i didn't realise that i can't even store it in that way, now it's clear to me why we need this abstract class :) – Karen Melikyan Oct 23 '18 at 19:12

2 Answers2

2

Shared pointers type erase destruction because they already have to handle reference counting. Going all the way to type erased destruction isn't that much more expensive.

Type erased destruction is required if you want to be able to convert shared pointers to T to shared pointers to void, which is useful sometimes. In general, it does away with the need that the data you store has a virtual destructor.

In addition, it permits an aliasing shared pointer that points within a resource owned by a shared pointer.

Your deleter obviously doesn't work; as it lacks a common base class, it can only be commonly stored in a void*, and you cannot invoke a void* with a (ptr).

Ignoring your shared::ptr, if we look at an industrial quality std::shared_ptr, we find that the reference control block stores the type-erased deleter at its end, and if you make_shared it does a trick to replace the type-erased deleter with an instance of the object.

struct rc_block {
  std::atomic<std::size_t> strong;
  std::atomic<std::size_t> weak;
  virtual void cleanup() = 0;
  virtual ~rc_block() {}
};

template<class T>
struct maked_rc_block final {
  std::atomic<std::size_t> strong = 1;
  std::atomic<std::size_t> weak = 0;
  std::aligned_storage<sizeof(T), alignof(T)> t;
  template<class... Args>
  maked_rc_block(Args&&...args) {
    ::new( (void*)&t ) T(std::forward<Args>(args)...);
  }
  void cleanup() override {
    ((T*)&t)->~T();
  }
};

template<class F>
struct action_rc_block final {
  std::atomic<std::size_t> strong = 1;
  std::atomic<std::size_t> weak = 0;
  F f;
  void cleanup() { f(); }
  template<class IN>
  actoin_rc_block(IN&& in):f(std::forward<IN>(in)) {}
};
template<class F>
action_rc_block(F)->action_rc_block<F>;

template<class T>
struct simple_shared {
  T* ptr = 0;
  rc_block* counters = 0;
  simple_shared( simple_shared const& o ):
    ptr(o.ptr), counters(o.counters)
  { if (counters) ++(counters->strong); }
  ~simple_shared() {
    if (counters && --(counters->strong)) {
      delete counters;
    }
  }
  template<class U>
  simple_shared(U* in):
    ptr(in),
    counters( new action_rc_block{[in]{ delete in; }} )
  {}
  // explicit deleter
  template<class U, class D>
  simple_shared(U* in, D&& d):
    ptr(in),
    counters( new action_rc_block{[in,d=std::forward<D>(d)]{ d(in); }} )
  {}
  template<class U, class V>
  simple_shared(simple_shared<U> const& alias_this, V* v):
    ptr(v),
    counters(alias_this.counters)
  {
    if(counters) ++(counters->strong);
  }
  template<class U>
  simple_shared( maked_rc_block<U>* c ):
    ptr( c?(T*)&c.t:nullptr ),
    counters(c)
  {}
};
template<class T, class...Args>
simple_shared<T> make_simple_shared( Args&&... args ) {
  auto* counter = new make_rc_block<T>( std::forward<Args>(args)... );
  return {counter};
}

My use of atomics is fast and loose, but I hope you get the idea.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
-1

The design space of owning smart pointers is large and diverse; but many design choices imply strong constraints.

For any shared owning smart pointer, the only alternative to storing a lambda do to delete static_cast<T*>(p); (or "type erasure") is to use delete on the last owning smart pointer in the group.

You could do that if you allow no conversion at all, and all owners have the exact same pointer value. That way, destruction of the last instance is guaranteed to always produce the same effect.

If you are allowing even derived to base conversion, it means that the result will only be well defined if the managed object has a virtual destructor. You can have support for static_cast and dynamic_cast on the pointer value.

It means that you can't have a custom deleter object that isn't part of the type of the smart pointer.

You can't construct an owning smart pointer that can have the control block allocated as part of the object (as make_shared), unless the smart pointer has a different type.

You don't have the generalized "aliasing constructor" so you can't get an owning smart pointer to an arbitrary subobject or object owned by the original object. (So if you have a smart pointer to a collection, you can't make an owning smart pointer to an element of the collection that keeps the collection alive while the element is needed.)

Going down this road (avoiding the cost of abstracting delete p;) will make the control block smaller and the deletion very slightly faster, but it will probably also cause a proliferation of specialized owning smart pointers of different incompatible types, because the other operations will often be needed in practice.

curiousguy
  • 8,038
  • 2
  • 40
  • 58