c++ shared_ptr in polymorphism without virtual destructor
Asked Answered
C

2

6

A typical factory design pattern require the base class to declare virtual destructor, but this can be actually avoided using shared_ptr.

#include <iostream>
#include <memory>
#include <cstdio>

using namespace std;

class Dog {
  public:
  ~Dog() { cout << "dog destroyed\n"; }
};

class Yellowdog : public Dog {
  public:
  ~Yellowdog() { cout << "Yellow dog destroyed.\n"; }
};

class DogFactory {
  public:
  static shared_ptr<Dog> createYellowdog() { 
    return shared_ptr<Yellowdog>(new Yellowdog()); 
  }
};

int main(int argc, char *argv[]) {

  auto ptr = DogFactory::createYellowdog();
  cout << ptr.use_count() << endl;

  return 0;
}

In this case the output is yellowdog destroyed followed by dog destroyed. But why? Why using shared_ptr can omit the virtual keyword before ~Dog?

Colored answered 28/4, 2016 at 16:52 Comment(3)
Because of the Deleter which is copied/transferred with the pointer.Franco
Because shared_ptr is magic. :-SLawson
To be more clear, the Deleter does something similar to delete (Yellowdog*) p;. You will get UB with return shared_ptr<Dog>(new Yellowdog()); though.Franco
Z
7

This is happening because shared_ptr stores type-erased deleter in the control block, which is created when the first shared_ptr is created. In your case shared_ptr is created for yellow dog, and deleter is to call yellow-dog destructor.

When you copy (or copy-construct) one shared_ptr to another, they share the same control block, and new shared ptr is going to call deleter from the original one - the one which will call yellowdog destructor.

However, it doesn't really make the class polymorphic and suitable for factory implementation - any other non-virtual function in the class will be called based on static type of shared_ptr, and you do not want this in polymorphic classes.

Zackaryzacks answered 28/4, 2016 at 17:11 Comment(1)
Is this behavior of shared_ptr mandated by the standard or is it just implementation-defined/unspecified?Frock
D
1

What SergeyA says is totally correct, for those who want to see the real code, here is the shared_ptr implementation from LLVM (I only show part of the code which are relevant for explaination):

template<class _Tp>
class _LIBCPP_SHARED_PTR_TRIVIAL_ABI _LIBCPP_TEMPLATE_VIS shared_ptr
{
    //Constructor taking a pointer
    template<class _Yp, class = _EnableIf<
        _And<__compatible_with<_Yp, _Tp>>::value> 
    >
    explicit shared_ptr(_Yp* __p) : __ptr_(__p) {
        unique_ptr<_Yp> __hold(__p);
        typedef typename __shared_ptr_default_allocator<_Yp>::type _AllocT;
        typedef __shared_ptr_pointer<_Yp*, __shared_ptr_default_delete<_Tp, _Yp>, _AllocT > _CntrlBlk;
        __cntrl_ = new _CntrlBlk(__p, __shared_ptr_default_delete<_Tp, _Yp>(), _AllocT());
        __hold.release();
        __enable_weak_this(__p, __p);
    }
...
//other code omited
}

//Move Constructor
template<class _Tp>
template<class _Yp>
inline
shared_ptr<_Tp>::shared_ptr(shared_ptr<_Yp>&& __r,
                            typename enable_if<__compatible_with<_Yp, element_type>::value, __nat>::type)
         _NOEXCEPT
    : __ptr_(__r.__ptr_),
      __cntrl_(__r.__cntrl_)
{
    __r.__ptr_ = nullptr;
    __r.__cntrl_ = nullptr;
}

Step 1: when you call shared_ptr<Yellowdog>(new Yellowdog());, first the constructor taking pointer will be called, _Tp is same as _Yp in this case. There a control block is also created, taking _Yp* (so Yellowdog*) as input, and saves it inside. Then the created control block is saved as its virtual interface (type-erased) inside shared_ptr

Step 2: Then during function return shared_ptr<Yellowdog> is converted to shared_ptr<Dog>. In this case, the move constructor of shared_ptr<Dog> is called, however, this time _Tp is Dog, _Yp is Yellowdog. so a implicit conversion from Yellowdog* to Dog* is happening for __ptr_. But for __ctrl_ it is a simple copy, nothing changes.

Step 3: During destruction, the deallocator from __ctrl_ will get called, and since it has saved Yellowdog* from the begining, it can call its destructor directly, and in turn it calls its parent (Dog) destructor. Therefore no virtual destructor is needed for Dog in this case.

Dragonroot answered 4/6, 2021 at 20:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.