Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why std::shared_ptr calls destructors from base and derived classes, where delete calls only destructor from base class? [duplicate]

Tags:

c++

shared-ptr

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.

like image 470
Piotr Wach Avatar asked Dec 27 '13 14:12

Piotr Wach


People also ask

Does derived destructor call base destructor?

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.

Why does a destructor in base class need to be declared virtual?

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.

In what order are the class destructors called when a derived class object is destroyed?

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).

Does shared pointer call destructor?

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.


2 Answers

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.

like image 90
Kerrek SB Avatar answered Oct 05 '22 23:10

Kerrek SB


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:

  • the static type of the pointer (shared_ptr<Base>)
  • the static type passed to the constructor
  • the actual dynamic type of the data

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.

like image 26
Matthieu M. Avatar answered Oct 06 '22 01:10

Matthieu M.