Why when using std::shared_ptr deallocation calls destructors from both base and derived classes when second example calls only destructor from base class?
class Base
{
public:
~Base()
{
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base
{
public:
~Derived()
{
std::cout << "Derived destructor" << std::endl;
}
};
void virtual_destructor()
{
{
std::cout << "--------------------" << std::endl;
std::shared_ptr<Base> sharedA(new Derived);
}
std::cout << "--------------------" << std::endl;
Base * a = new Derived;
delete a;
}
Output:
--------------------
Derived destructor
Base destructor
--------------------
Base destructor
I was expecting the same behaviour in both cases.
A derived class's destructor (whether or not you explicitly define one) automagically invokes the destructors for base class subobjects. Base classes are destructed after member objects.
A virtual destructor is needed when you delete an object whose dynamic type is DerivedClass by a pointer that has type BaseClass* . The virtual makes the compiler associate information in the object making it able to execute the derived class destructor. Missing the virtual in such case causes undefined behavior.
The body of an object's destructor is executed, followed by the destructors of the object's data members (in reverse order of their appearance in the class definition), followed by the destructors of the object's base classes (in reverse order of their appearance in the class definition).
When a shared_ptr object goes out of scope, its destructor is called. Inside its destructor it decrements the reference count by 1 and if new value of reference count is 0 then it deletes the associated raw pointer. To delete the internal raw pointer in destructor, by default shared_ptr calls the delete() function i.e.
delete a
is undefined behaviour, because the class Base
does not have a virtual destructor and the "complete object" of *a
(more accurately: the most-derived object containing *a
) is not of type Base
.
The shared pointer is created with a deduced deleter that deletes a Derived *
, and thus everything is fine.
(The effect of the deduced deleter is to say delete static_cast<Derived*>(__the_pointer)
).
If you wanted to reproduce the undefined behaviour with the shared pointer, you'd have to convert the pointer immediately:
// THIS IS AN ERROR
std::shared_ptr<Base> shared(static_cast<Base*>(new Derived));
In some sense, it is The Right Way for the shared pointer to behave: Since you are already paying the price of the virtual lookup for the type-erased deleter and allocator, it is only fair that you don't then also have to pay for another virtual lookup of the destructor. The type-erased deleter remembers the complete type and thus incurs no further overhead.
A missing piece to Kerrek SB's answer is how does the shared_ptr
knows the type ?
The answer is that there are 3 types involved:
shared_ptr<Base>
)And shared_ptr
does not know of the actual dynamic type, but knows which static type was passed to its constructor. It then practices type-erasure... but remembers somehow the type. An example implementation would be (without sharing):
template <typename T>
class simple_ptr_internal_interface {
public:
virtual T* get() = 0;
virtual void destruct() = 0;
}; // class simple_ptr_internal_interface
template <typename T, typename D>
class simple_ptr_internal: public simple_ptr_internal_interface {
public:
simple_ptr_internal(T* p, D d): pointer(p), deleter(std::move(d)) {}
virtual T* get() override { return pointer; }
virtual void destruct() override { deleter(pointer); }
private:
T* pointer;
D deleter;
}; // class simple_ptr_internal
template <typename T>
class simple_ptr {
template <typename U>
struct DefaultDeleter {
void operator()(T* t) { delete static_cast<U*>(t); }
};
template <typename Derived>
using DefaultInternal = simple_ptr_internal<T, DefaultDeleter<Derived>>;
public:
template <typename Derived>
simple_ptr(Derived* d): internal(new DefaultInternal<Derived>{d}) {}
~simple_ptr() { this->destruct(); }
private:
void destruct() { internal->destruct(); }
simple_ptr_internal_interface* internal;
}; // class simple_ptr
Note that thanks to this mechanism, shared_ptr<void>
is actually meaningful and can be used to carry any data an properly dispose of it.
Note also that there is a penalty involved with this semantics: the need for indirection required for the type-erasure of the deleter
attribute.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With