Почему 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
.