Почему std:: shared_ptr вызывает деструкторы из базового и производного классов, где delete вызывает только деструктор из базового класса?

Почему при использовании std:: shared_ptr deallocation звонит деструкторам из базового и производного классов, когда второй пример вызывает только деструктор из базового класса?

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;
}

Вывод:

--------------------
Derived destructor
Base destructor
--------------------
Base destructor

Я ожидал такого же поведения в обоих случаях.

Ответ 1

delete a - это поведение undefined, потому что класс Base не имеет виртуального деструктора, а "полный объект" *a (точнее: самый производный объект, содержащий *a), не является типа Base.

Общий указатель создается с выведенным делетером, который удаляет Derived *, и, следовательно, все в порядке.

(Эффект выведенного делетира состоит в том, чтобы сказать delete static_cast<Derived*>(__the_pointer)).

Если вы хотите воспроизвести поведение undefined с общим указателем, вам придется немедленно преобразовать указатель:

// THIS IS AN ERROR
std::shared_ptr<Base> shared(static_cast<Base*>(new Derived));

В некотором смысле, это правильный путь для общего указателя: поскольку вы уже платите цену за виртуальный поиск для стираемого и распределяемого по типу устройства, справедливо, что вы также не придется заплатить за другой виртуальный поиск деструктора. Устранивший тип депилятор помнит полный тип и, следовательно, не наносит дополнительных накладных расходов.

Ответ 2

Отсутствующая часть ответа Kerrek SB - это способ, которым shared_ptr знает тип?

Ответ заключается в том, что есть 3 типа:

  • статический тип указателя (shared_ptr<Base>)
  • статический тип, переданный конструктору
  • фактический динамический тип данных

И shared_ptr не знает фактического динамического типа, но знает, какой статический тип был передан его конструктору. Затем он практикует стирание стилей... но как-то помнит тип. Пример реализации будет (без совместного использования):

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

Обратите внимание, что благодаря этому механизму shared_ptr<void> на самом деле имеет смысл и может использоваться для переноса любых данных, которые его правильно утилизируют.

Обратите внимание также, что в этой семантике есть наказание: необходимость косвенности, требуемая для стирания типа атрибута deleter.