Каковы последствия уничтожения объекта производного класса через std::unique_ptr к базовому классу, если у базового деструктор невиртуальный?
Если объект производного класса уничтожается через std::unique_ptr к базовому классу, а деструктор базового класса невиртуальный, поведение программы неопределено. Для полиморфного владения базовый класс должен иметь виртуальный деструктор либо должен использоваться специальный удалитель, корректно знающий фактический тип объекта.
В C++ удаление объекта через указатель на базовый класс предназначено для полиморфной иерархии. Виртуальные функции позволяют выбрать реализацию по динамическому типу объекта; виртуальный деструктор распространяет этот принцип на корректное уничтожение.
std::unique_ptr появился как RAII-обёртка, чтобы автоматически освобождать ресурс при выходе владельца из области видимости, включая выход из-за исключения. Однако RAII не исправляет ошибочную модель уничтожения: умный указатель всё равно вызывает удалитель для своего статического типа.
Пусть std::unique_ptr<Base> фактически владеет объектом Derived. При уничтожении указателя его стандартный удалитель выполняет удаление через Base*. Если деструктор Base не виртуальный, механизм не обязан корректно вызвать цепочку деструкторов производного объекта.
Последствия включают неопределённое поведение: от утечки ресурсов, которыми владеет Derived, до повреждения памяти и аварийного завершения. Даже если простой тест проходит, это не делает программу корректной.
Для полиморфной иерархии деструктор базового класса делают виртуальным:
При уничтожении object сначала вызывается деструктор Derived, затем Base. Это гарантирует освобождение ресурсов, принадлежащих производной части, а затем базовой части.
Важно отличать виртуальность обычных методов от виртуальности деструктора. Наличие других виртуальных функций само по себе не означает, что уничтожение через Base* будет корректным: деструктор должен быть виртуальным явно или косвенно присутствовать в виртуальной иерархии как виртуальный деструктор.
Если базовый тип не предназначен для удаления через него, можно сделать его деструктор непубличным и запретить такой способ владения. Тогда объект должен уничтожаться фабрикой, специальным владельцем или удалителем, который использует корректный конкретный тип. Это ограничивает интерфейс, но предотвращает ошибочное удаление.
Кастомный удалитель также может решить задачу, если он гарантированно знает фактический тип объекта. Компромисс заключается в усложнении типа std::unique_ptr, фабрики и передачи владения; для обычной полиморфной иерархии виртуальный деструктор обычно проще и прозрачнее.
В системе загрузки обработчиков файл фабрика возвращает std::unique_ptr<Обработчик>, но у Обработчик был объявлен виртуальный метод обработать и невиртуальный деструктор. Конкретный обработчик содержал файловый дескриптор, поэтому при уничтожении через базовый указатель его деструктор не вызывался корректным образом.
Рассматривались три варианта. Возврат сырого указателя оставлял управление временем жизни вызывающему коду и повышал риск утечки; ручной вызов специального метода освобождения был хрупким; кастомный удалитель сохранял невиртуальный интерфейс, но усложнял фабрику и тип владения.
Выбрали виртуальный деструктор в базовом классе. Это сохранило std::unique_ptr как единственного владельца, сделало уничтожение полиморфным и устранило зависимость корректности от ручных соглашений.
Нет. Вызов выполняется через тип базового указателя, поэтому деструктор базового класса должен быть виртуальным. Виртуальность деструктора производного класса не исправляет невиртуальный деструктор базового.
Да, формально возникает неопределённое поведение независимо от текущего состава полей производного класса. Сегодня производный класс может быть пустым, но его реализация или наследник завтра может получить ресурс; полагаться на отсутствие видимых последствий нельзя.
std::unique_ptr<Base>, если создание выполняется через std::make_unique<Derived>()?Да, если деструктор Base виртуальный. std::make_unique<Derived>() создаёт владельца конкретного типа, а преобразование к std::unique_ptr<Base> передаёт владение; корректность последующего уничтожения определяется возможностью безопасно удалить объект через Base*, то есть виртуальным деструктором или специально заданным удалителем.