Pull to refresh

Comments 51

Хотелось бы уточнить по поводу:
> Вывод: делать виртуальные методы публичными не очень хорошо.
Это вы имеете ввиду конечные классы, которыми пользуется пользователь, или вообще всегда? Ведь абстрактные классы никто не запрещал вроде.
<gramar_nazi>
У Вас в заголовке запятая пропущена
</gramar_nazi>
Честно говоря, в любой разработке один из самых неуловимых, но важных моментов — соблюдать меру. Если вы пишете корпоративное приложение, которое и поддерживаете — то да, подход вашей «иномарки» оправдан, т.к. позволяет вам иметь контроль над приложением и держать архитектуру чистой.

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

Так что… Да, есть минусы, есть плюсы. Но конкретный выбор далеко не столь однозначен и прост.
> Любопытно было бы узнать, а какой ответ ожидали они?

Вероятно, достаточно бы было ответа про:
1) В C++ main должен возвращать int
2) В списках инициализаторов Bar должен быть Foo(что-то там), поскольку у Foo нет конструктора без обязательных параметров.
3) В Foo должен быть конструктор копирования
4) В Bar должен быть конструктор неявного преобразования в Foo (привет valgrind)
5) Для реализации пункта 4 скорее всего понадобится указать спецификатор доступа к родительскому классу class Bar: public Foo
6) В ~Foo и ~Bar должны быть delete[], а не delete
7) Проверки на успешность выделения памяти? fputs("Доставьте ваш комп в музей", stderr); Тут почти наверняка они хотели увидеть что-то про auto_ptr и SFINAE
8) В качестве параметров конструкторов должно быть size_t, а не int (привет статические анализаторы)
9) Виртуальные деструкторы: см. пост.

Хотя, быть может, они хотели увидеть переделанный код на std::vector и отсутствие элементов-данных с одинаковыми именами. Чёрт их разберёт.
Даже по первому пункту можно сказать много больше. В общем, по однму только первому вопросу можно докторскую защищать :-) Такую добротную, с историей вопроса, обзором литературы и стандартов, описанием существующих решений и тенденций.
> fputs(«Доставьте ваш комп в музей», stderr);

никогда не вылезали за 2Gb в 32-битном процессе? бывают такие задачи.
UFO just landed and posted this here
Для выполнения предложенной задачи необходимо и достаточно 500 байт на классической 32-битной платформе. Так что точно в музей =).

Ещё: к пункту 8 скорее всего стоит дописать замену int *i на int32_t *i.

Задача-минимум состоит в том, чтобы код компилировался компиляторами, работающими по стандарту, не производил утечек и обеспечивал одинаковую работу на разных платформах/архитектурах.
Мастеров пихающих int32_t везде без разбора надо пороть.
3: в Foo по той же причине должен быть ещё и оператор присваивания. И так же конструктор копирования и оператор присваивания должны быть в Bar.

7: operator new кидает исключение при невозможности выделить память.
На картинке явно видно, что строение не жилым, не промышленным и никаким небыло, построили и сразу поломали?

— Что это за семиэтажная херня?
— Дак вы же сами…
— Экскаватор на нее поставили и чтоб я через 3 дня ее не видел!
Это снос какого-то небоскрёба в Китае. Был ли он жилим — не знаю.
Это глубокий рефакторинг на раннем этапе развития проекта :)
Не соглашусь с таким шаблонным разделением: «маленькое и ненадолго — публичный виртуальный» против «большое и всерьёз — защищённый невиртуальный». Есть паттерны, опирающиеся на полиморфный деструктор, state например. И Герб Саттер в своей заметке не так категоричен. Нужно просто понимать что виртуальный и невиртуальный деструктор — предназначены для двух разных вариантов использования базового класса.
Как-то уж очень много натяжек
Вывод: полиморфное удаление — подозрительная штука.
Вывод сделан на основании того, что владеющий класс всегда контролирует процесс создания объектов — то есть что не бывает сложных объектов, созданных на классе-фабрике и переданных во владение в нашу подсистему. Ага.

Вывод: делать виртуальные методы публичными не очень хорошо.
Замечательно — особенно если мы реализуем COM-интерфейс с какими-нибудь тривиальными свойствами. Что там надо городить вместо простой реализации
virtual int GetTrivialField() const = 0
?

Вывод: прежде, чем сделать первый виртуальный метод, можно на минуту задуматься: а оно нам точно надо?
Ограничение по поводу union'ов конечно нас пугает до смерти, а уж использование виртуальных вызовов если боремся за скорость проконтролировать никак нельзя. Ага.

Предложенное переусложнение, IMHO, не решает никаких проблем. Так что лучше пользоваться виртуальными деструкторами и не мучиться бессонницей на эту тему.
> автоматизировать процесс удаления объектов (автоматические переменные, auto_ptr и множество подобных средств),

Давайте, расскажите мне как использовать auto_ptr на интерфейс без полиморфного удаления

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

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

> приблизить друг другу операции создания и удаления и таким образом сделать код более простым, стройным.

Да что вы говорите. Каким образом код станет проще и стройнее? Создание и удаление в современном C++ — это RAII, new и delete действительно рядом написаны в коде, но вот только в большом количестве случаев — удаление полиморфное: shared_ptr(new Service); при этом создание и удаление производится как правило в разных контекстах

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

Еще есть ограничения, кроме непереносимого юниона? дальше вроде о переносе на другие платформы упоминается.

Итого: стоящих аргументов не видно, примеров плохого кода с полиморфным удалением нет.

ааа, парсер жрет угловые скобки. Еще раз: shared_ptr &lt IService &gt (new Service)
Вывод: делать виртуальные методы публичными не очень хорошо.


Насчет обычных методов спорить не буду, а вот деструктор практически всегда должен быть виртуальным, так как теоретически у вас нету 100-процентной уверенности в том, что кто-нибудь не захочет наследовать от вашего класса.

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

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

Тогда уж лучше так: еcли у наследника есть деструктор, то у базового класса деструктор должен быть виртуальным. Только вот беда — базовый класс заранее не знает, какие от него отнаследуются наслденики.

> необходимо в комментарии пояснить, что он не предназначен для наследования

Во-первых, класс не предназначен для полиморфного удаления. А наследовать можно и даже нужно.

Во-вторых, обратите внимание, что если вы делаете деструктор защищённым, то никаких комментариев не понадобится. Код из первого примера просто не скомпилируется.
в java все методы виртуальные
и ничего — живут люди
С таким же успехом можно сказать: «в Perl все методы публичные, и ничего — живут люди».
Java — это другой язык, в нём другие средства. Ваши люди (которые «живут же») ведь не брезгуют использованием интерфейсов?
либо следует удалять объекты где-то не слишком далеко от точки их здания

исправьте здания на создания
Один из случаев, необходимо использование виртуальных деструкторов — придание виртуальности оператору delete, определенному локально. Язык C++ не является полным, поэтому приходится так неочевидно добиваться virtual operator delete.
По поводу вопроса в вакансии Яндекса — тип подкласса из-за private наследования(т.е. наследование реализации) не является полиморфным и не приводится к базовому классу, соответственно то что мы обсуждаем немного не то.

В STL практически нет виртуальных функций и деструкторов потому что это часто не нужно. И зачем использовать контейнер STL как полиморфный тип?

«Деструкторы необходимо делать виртуальными.» — так говорят всегда всем студентам и школьникам при первом знакомстве с С++. В этом контексте, я полностью согласен с этим утверждением — если сделаешь деструктор виртуальным, то хуже никогда не будет. Понимая как он работает, ты вряд ли ошибешься.

Но отсутствие виртуальности деструктора позволяет:
— сделать базовый класс POD-типом
— сделать С-совместимый тип (иногда)
— уменьшить растраты на VMT(иногда, с увеличением производительности)
— использовать объект без его удаления в callback-ах

Если забыть про разные С-подобные штуки, то вопрос точнее должен стоять так — А должны ли объекты класса быть полиморфно удаляемыми?
Для COM-объектов это необходимо, вспомните про delete this в Release().
Вот этот тонкий момент: «сделать базовый класс POD-типом» + «сделать С-совместимый тип» + уменьшить растраты на VMT меня всегда волновал. А оно надо? Если идёт борьба за байты и миллисекунды, может просто написать критически важные участки кода на чистом с, или вообще на асме?

А последний пункт («использовать объект без его удаления в callback-ах») не понял, объясните, пожалуйста.
но ведь С++ удобнее, и в нем есть много бесплатных удобств…
а некоторые удобства еще и несут дополнительную производительность, шаблоны например…
Ну а пайтон (руби, js...) ещё удобнее. Вопрос то не в удобстве, а в производительности. Вот критические куски можно переписывать на чистом си.

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

Обращаю внимание, что из первого предложения следует отрицание второго. Если то, что создаётся в конструкторе логично освободить в деструкторе, то если в потомках что-то будет создаваться (что довольно ожидаемо), то деструкторы потомков должны вызываться и деструктор должен быть виртуальным.

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

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

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

И совершенно не понятно, как наличие не публичных виртуальных методов может помешать наследовать интерфейс?

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

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

Если перейти именно к архитектуре, то хотелось бы задать вопрос, как реализовать работу плагинов без полиморфизма, как реализовать работу с различными объектами на карте (если конкретно по вакансии)?
Использовать case?

Высказывание относительно того, что использовать открытый виртуальный метод, это на мой взгляд тоже самое, что и зачем использовать руль в машине, машина нужна для того, чтобы ехать, а не для того, чтобы рулить.
По поводу заголовка, кажысь правильнее было бы написать «Как безопасно разрушить объект» и другие мысли.
typo: не правильный деструктор -> неправильный деструктор
UFO just landed and posted this here
По поводу shared_ptr — это не так в случае полиморфизма, поскольку тогда он объявляется для абстрактного класса, а ссылается на конкретные объекты, и остаются все те же проблемы, что и с обычным указателем.
UFO just landed and posted this here
Да, вы правы.
Спасибо, не знал.
A *get_a() { return new B; }
...
    std::shared_ptr<A> a(get_a());

UFO just landed and posted this here
Хорошая статья — удачное жонглирование псевдологикой. Элементарные и корректные примеры… не относящиеся к теме. Все остальное — метафоры, «не вызывает ни у кого сомнений», «не есть хорошо и может выйти вам боком» и прочие откровения британских ученых.
Единственный корректный аргумент во всей статье — не может использоваться в объединениях. Ну ради объединений не грех отказаться от ООП.
Юзать union'ы в 2010(11) году! Это мягко говоря моветон
у меня от статьи когнитивный диссонанс какой-то, простите.
А может это вброс такой?
Каждый год, 31-го декабря, мы с друзьями красим вентилятор… ;)
По поводу деструкторов моя мысль проста.
Есть хоть один виртуальный метод — сразу же обзаводись виртуальным деструктором!
Если нет — надо думать, на что класс годится; обычно не нужно.
+1, в GCC можно включить warning для таких случаев…
UFO just landed and posted this here
Есть мнение, что стоит вообще отказаться от выделения и освобождения ресурсов в конструкторахи деструкторах объектов в пользу явных вызовов и специализированных интерфейсов и аллокаторов по причине трудностей, возникающих при обработке исключительных ситуаций.
Кстати, не стоит еще забывать и о том, что использование виртуальных методов необходимо для обеспечения ABI (Application Binary Interface) совместимости, при разработки динамических (so/dll) библиотек.
Sign up to leave a comment.

Articles