Pull to refresh

Как безопасно разрушить объект. И другие мысли

Reading time 5 min
Views 12K
Недавно разглядывал вакансии одной известной конторы, задумывался над вопросам (которые, кстати, на всех их вакансиях одинаковые). И решил написать заметку по самому интересному (на мой взгляд) аспекту первого же вопроса. Может быть доберусь и до других, а пока предлагаю задуматься, надо ли делать деструкторы виртуальными?

Ответ не так уж однозначен, и чтобы заманить вас под кат скажу, что в реализации STL вы обнаружите всего несколько виртуальных деструкторов.

Каким же должен быть полный ответ на вопрос про деструкторы?


Суть проблемы для тех, кто не очень в курсе



Итак, привожу, всем уже надоевший пример, представляющий неправильный деструктор:

#include <iostream>

class A {
public:
    A() {std::cout << "A()" << std::endl;}
    ~A() {std::cout << "~A()" << std::endl;} // 6
};

class B : public A {
public:
    B() {std::cout << "B()" << std::endl;}
    ~B() {std::cout << "~B()" << std::endl;}
};

int main() {
    A *a = new B; // 16
    delete a;     // 17
    return 0;
}

Результат таков:

A()
B()
~A()

То есть, конструктор B в строке 16 честно вызвал оба конструктора (A и B), а деструктор в строке 17 вызвал только деструктор класса A, что полностью согласуется с типом переданной ему ссылки.

Всё отработало правильно, но деструктор B вызван не был, что могло привести к утечкам памяти, дескрипторов и других полезных ресурсов.

Как с этим бороться?


Общепринятый ответ — виртуальный деструктор



Если в строке 6 мы добавим слово virtual:

virtual ~A() {std::cout << "~A()" << std::endl;} // 6

то всем будет счастье

A()
B()
~B()
~A()

Это обсуждалось на хабре много раз. Были очень хорошие статьи. Но я хотел бы обсудить не то, как работают виртуальные методы, а более философские вопросы.


Хорошо ли делать публичными виртуальные методы?



Давайте вспомним, для кого создаются публичные методы. Они определяют интерфейс класса и создаются для тех, кто будет использовать класс.

А для чего существуют виртуальные методы? Правильно — для настройки поведения класса. То есть для тех, кто будет расширять функциональность класса.

Наверно любой здравомыслящий человек скажет, что использование класса и разработка класса — это разные задачи. Смешивать их не нужно. Поэтому и придуманы подходы NVI (Non-Virtual Interface), поведенческие патерны типа bridge и другие уловки, позволяющие разделить абстракцию и реализацию. Полезность такого разделения уже не вызывает ни у кого сомнений. Мы не будем здесь подробно описывать все детали и варианты, а лишь сделаем вывод.

Вывод: делать виртуальные методы публичными не очень хорошо.

Если вы испытываете острую необходимость в публичных виртуальных методах, это сигнал, что у вас слишком жёстко связаны интерфейсы и их реализации, что не есть хорошо и может выйти вам боком.

Второй вопрос:


Хорошо ли полиморфно уничтожать объекты?



Как известно, всё, что было создано, надо бы удалить. Чтобы не запутаться, не ошибиться, ничего не забыть и не пропустить надо либо автоматизировать процесс удаления объектов (автоматические переменные, auto_ptr и множество подобных средств), либо следует удалять объекты где-то не слишком далеко от точки их создания.

Скажем, если вы создаёте объект (в куче) в конструкторе некого контейнера, то уместно удалить этот объект в деструкторе того же контейнера. Обратите внимание, что в данном случае в полиморфном удалении нет никакой необходимости.

Вывод: полиморфное удаление — подозрительная штука.

Если вы испытываете острую необходимость в полиморфном удалении — это повод задуматься над дизайном. На самом деле полиморфный деструктор нужен не так часто. Возможно следует чуть пересмотреть дизайн, отказаться от полиморфного удаления, приблизить друг другу операции создания и удаления и таким образом сделать код более простым, стройным. Пусть объект живёт полиморфной судьбой, но создание и удаление — это парные операции. Объект создаётся вполне определённого типа и логично и удалить его не забывая про этот тип. Это возможно не всегда, но очень часто.


Кроме того, виртуальные методы сразу создают некоторые ограничения



Не будем на этом подробно останавливаться, но наличие хотя бы одного виртуального метода создаёт некоторые дополнительное накладные расходы и накладывает ограничения. Например, такие объекты нельзя использовать в объединениях.

Вывод: прежде, чем сделать первый виртуальный метод, можно на минуту задуматься: а оно нам точно надо?


Так каким же должен быть идеальный деструктор базового класса?



Иносказательно.

Делать деструкторы не виртуальными и публичными (как в первом примере) — всё равно, что ездить на роликах по МКАД. Одно не ловкое движение, не успели увернуться и вот уже ролики отдельно, а вы отдельно…

Делать деструктор публичным и виртуальным — это как ездить по МКАД на асфальтоукладочном катке. Вам ничего не грозит, вы как в танке. Можете нарезать круги, не опасаясь за свою жизнь. Но это не самый быстрый способ; к тому же, ваше движение вступает в некоторое противоречие с общим потоком.

Делать деструктор защищённым и не виртуальным, это как двигаться по МКАД на бронированной иномарке с кондиционером, личным водителем за рулём и бокалом шампанского в руках. Это не только безопасно и удобно для вас, вы ещё и не создаёте неудобств для других. Хотя (обратите внимание) на вашей иномарке вы не сможете проехать везде, где проехал бы каток.

Защищённый не виртуальный деструктор хорош почти всем. Он не виртуален и он не позволяет пользователям класса создавать аварийные ситуации на подобии той, что была показана в первом примере. Он не позволяет использовать базовый класс напрямую, что тоже часто бывает полезно. Одним словом, он защищает вас от ошибок и вынуждает писать более качественный код.

Асфальтоукладчики — хорошая штука. Иногда для работы нужны действительно именно они. Но это не единственный вид транспорта и, более того, часто удобнее пользоваться не ими.

Итак ответ (полный):

Если вы пишите что-то маленькое и не на долго — делайте деструкторы публичными и виртуальными и ни о чём не думайте. Ещё не было случаев, чтобы виртуальные деструкторы кого-то подвели. Это надёжно, и в этом нет ничего плохого.

Если вы создаёте софт на годы с перспективой развития (совершенствования, переноса на другие платформы), то следует серьёзно подумать о защищённых деструкторах. Чёткое раздение абстракции и реализации — полезная штука.


Всем счастливых праздников и успехов в новом году!



Спасибо известной компании за интересные вопросы. Любопытно было бы узнать, а какой ответ ожидали они? Уж очень маленькое поле ввода для ответа. А ведь я коснулся лишь одной части ответа на их вопрос.

upd: появился ответ на эту статью. Отвечать наверно ничего не буду. Диагноз мне там уже поставили. Авторитетов автор, очевидно, не признаёт. Книжки для него не аргумент. Но в конструктивной части заметки есть очень правильные мысли о вреде наследования вообще. А что касается фабрики кодеков, о которой пишет автор в конце, то он может надеяться почитать рассказ и про это, если я соберусь написать про вопрос 4 из всё того же наборчика вопросов.

upd2: Почему-то находятся люди, которые воспринимают этот пост как нападку на Яндекс. Уверяю вас! В этом посте нет скрытого смысла и нет нападок. Я просто почитал их вопросы, они мне показались достойными обсуждения, я написал заметочку. И всё.
Tags:
Hubs:
+37
Comments 51
Comments Comments 51

Articles