0

I am creating my custom shared pointer class and I want my shared pointer class should call derived class destructor when it goes out of scope for the below code.

...
...
template<class T>
MySharedPtr<T>::MySharedPtr(T * p) : ptr(p), refCnt(new RefCount())
{
    refCnt->AddRef();
}

template<class T>
void MySharedPtr<T>::release()
{
    if (refCnt->Release() == 0) 
    {
        delete ptr;
        delete refCnt;
    }
    ptr = nullptr;
    refCnt = nullptr;
}
...
...

Base class destructor called when it goes out of scope but if I use std::shared_ptr<Base> bptr(new Derived());, it calls derived destructor and base destructor when it goes out of scope. How can I achieve the same behaviour with my custom class?

  class Base
    {
    public:
        
        Base() {
            cout << "Base default constructor" << endl;
        }
        ~Base() {
            cout << "Base destructor" << endl;
        }
        virtual void display() {
            cout << "in Base" << endl;
        }
    };
    
    class Derived : public Base
    {
    public:
        Derived() {
            cout << "Derived default constructor" << endl;
        }
        ~Derived() {
            cout << "Derived destructor" << endl;
        }
        virtual void display() {
            cout << "in Derived" << endl;
        }
    };
    int main()
    {
        MySharedPtr<Base> bptr(new Derived());
        bptr->display();
    }
Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • 2
    Why isn't the destructor virtual ? – wohlstad May 30 '22 at 16:12
  • Related to: https://stackoverflow.com/questions/3899790/shared-ptr-magic – Eljay May 30 '22 at 16:18
  • @Frank I think it works well (i.e. calls derived dtor) even if you use `std::shared_ptr bptr(new Derived());` which is of type `std::shared_ptr`. I think it's part of the shared_ptr magic @Eljay pointed above. – wohlstad May 30 '22 at 16:20
  • A class with virtual functions should always have a virtual destructor for exactly that reason. But if you don't then you need a custom deleter that `dynamic_casts``the `Base*` to `Derived*` and deletes that. You end up just implementing a virtual destructor by hand. – Goswin von Brederlow May 30 '22 at 17:34
  • *"How can I achieve the same behaviour with my custom class?"* -- given the "magic", it might be informative to take this the other way. How can one achieve your flawed behavior with `std::shared_ptr`? One way is to replace `MySharedPtr bptr(new Derived());` with two lines: `Base * raw_ptr = new Derived();` and `shared_ptr bptr(raw_ptr);`. ***Note:** for demonstration only; I am not recommending this as a general practice.* – JaMiT May 30 '22 at 17:39

1 Answers1

1

You can store a callback as part of the RefCount object which will be called when the reference count goes to zero. This callback can "remember" what it needs to do based on the pointer type that was used to originally construct the MySharedPtr object, even though the knowledge of that most derived type might have been lost elsewhere.

You must modify the constructor of MySharedPtr so that the type information is not immediately destroyed when the constructor is called. It needs to take U*, not T*. If it takes T*, then as soon as the constructor is entered, you already don't know what the original argument type was, since it has been converted to T*.

template <class T>
class MySharedPtr {
  public:
    template <class U>
    MySharedPtr(U* p);
  // ...
};

template <class T>
template <class U>
MySharedPtr<T>::MySharedPtr(U* p) : ptr(p), refCnt(new RefCount(p))
{
    refCnt->AddRef();
}

The RefCount constructor needs to set up the stored callback based on this pointer:

class RefCount {
  public:
    template <class U>
    RefCount(U* p) : deleter_{[p]{ delete p; }} {}
    // ...
    void invoke_deleter() { deleter_(); }

  private:
    int ref_count_ = 0;
    std::function<void()> deleter_;
};

So now, if you do:

MySharedPtr<Base> bptr(new Derived());

the Derived* will be passed to the RefCount constructor, and it will store a callback that calls delete on that pointer (of type Derived*) and not the ptr stored in the MySharedPtr, which is of type Base*.

You need to add the code to invoke this deleter when the ref count goes to 0:

template<class T>
void MySharedPtr<T>::release()
{
    if (refCnt->Release() == 0) 
    {
        refCnt->invoke_deleter();
        delete refCnt;
    }
    ptr = nullptr;
    refCnt = nullptr;
}

This is basically how std::shared_ptr works, although I suspect it doesn't use std::function internally, but some custom type erasure mechanism with lower overhead since it doesn't need to support most std::function features.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312