Чтобы предложить обратную сторону к общему совету (который звучит, когда нет явных проблем с детализацией / производительностью) ...
Сценарий разумного использования
По крайней мере, естьодин сценарий, в котором хорошим выходом может быть публичное получение из баз без виртуальных деструкторов:
- вам нужны некоторые преимущества безопасности типов и читаемости кода, предоставляемые выделенными пользовательскими типами (классами)
- существующая база идеально подходит для хранения данных и допускает низкоуровневые операции, которые клиентский код также хотел бы использовать
- , вам нужно удобство повторного использования функций, поддерживающих этот базовый класс
- вы понимаете, что любые дополнительные инварианты, в которых ваши данные логически необходимы, могут быть реализованы только в коде, явно обращающемся к данным как производному типу, и в зависимости от того, в какой степени это "естественно" произойдет в вашем проекте, и от того, насколько вы можете доверятьклиентский код, чтобы понять и сотрудничать с логически-идеейВо всех инвариантах вы можете захотеть, чтобы функции-члены производного класса переоценивали ожидания (и выбрасывали, или что-то в этом роде)
- производный класс добавляет некоторые специфичные для типа удобные функции, работающие с данными, такие как пользовательский поиск, фильтрация данных/ модификации, потоковая передача, статистический анализ, (альтернативные) итераторы
- связывание клиентского кода с базой более уместно, чем связывание с производным классом (поскольку база либо стабильна, либо изменения в ней отражают также улучшения функциональностиядро к производному классу)
- Другими словами: вы хотите, чтобы производный класс продолжал предоставлять тот же API, что и базовый класс, даже если это означает, что клиентский код вынужден измениться, а не изолировать его вкаким-то образом, позволяющим базовому и производному API-интерфейсу расти синхронно
- , вы не собираетесь смешивать указатели на базовые и производные объекты в частях кода, отвечающих за их удаление
Это может звучать какЭто ограничительно, но в реальных программах, соответствующих этому сценарию, есть множество случаев.
Фоновое обсуждение: относительные достоинства
Программирование - это компромиссы.Прежде чем написать более концептуально «правильную» программу:
- подумайте, требует ли она дополнительной сложности и кода, который запутывает реальную логику программы, и, следовательно, в целом более подвержен ошибкам, несмотря на более надежную обработку одной конкретной проблемы,
- сопоставьте практические затраты с вероятностью и последствиями проблем, а
- рассмотрите "возврат инвестиций" и то, что еще вы могли бы делать со своим временем.
Если потенциальные проблемы связаны с использованием объектов, которые вы просто не можете себе представить, кто-нибудь попытается дать вам представление об их доступности, объеме и характере использования в программе , или вы можете генерировать ошибки во время компиляции дляопасное использование (например, утверждение, что размер производного класса совпадает с размером базы, что предотвратит добавление новых членов данных), тогда все остальное может быть преждевременным чрезмерным проектированием.Возьмите легкий выигрыш в чистом, интуитивно понятном, лаконичном дизайне и коде.
Причины рассмотреть деривацию без виртуального деструктора
Допустим, у вас есть класс D, публично полученный из B. Без усилий, операциина B возможны на D (за исключением конструирования, но даже если имеется много конструкторов, вы часто можете обеспечить эффективную пересылку, имея один шаблон для каждого отдельного числа аргументов конструктора: например, template <typename T1, typename T2> D(const T1& x1, const T2& t2) : B(t1, t2) { }
. Лучшее обобщенное решение в C++ 0x variadic шаблонов.)
Кроме того, если B изменяется, то по умолчанию D предоставляет эти изменения - оставаясь в синхронизации - но кому-то может понадобиться просмотреть расширенную функциональность, введенную в D, чтобы увидеть, остается ли она действительной, и использование клиента.
Перефразируя это: уменьшена явная связь между базовым и производным классом, но увеличена связь между базой и клиентом .
Это часто НЕ то, что вы хотите, но иногда это идеально, а иногда нет проблем (см. Следующий параграф). Изменения в базе приводят к большему количеству изменений клиентского кода в местах, распределенных по всей базе кода, и иногда люди, изменяющие базу, могут даже не иметь доступа к клиентскому коду для его просмотра или обновления соответственно. Однако иногда лучше: если вы как поставщик производного класса - «человек посередине» - хотите, чтобы изменения базового класса передавались клиентам, и вы обычно хотите, чтобы клиенты могли - иногда вынуждены - обновлять свой код, когда изменения в базовом классе без необходимости постоянного участия, тогда публичный вывод может быть идеальным. Это часто случается, когда ваш класс является не столько самостоятельной сущностью, сколько тонким дополнением к базе.
В других случаях интерфейс базового класса настолько стабилен, что соединение может рассматриваться как не проблема. Это особенно верно для классов, таких как стандартные контейнеры.
В общем, общедоступная деривация - это быстрый способ получить или приблизить идеальный, знакомый интерфейс базового класса для производного класса - таким кратким и самоочевидным образом корректным как для сопровождающего, так и для клиентского кодера - с дополнительными функциями, доступными как функции-члены (которые, по-моему, отличаются от Sutter, Alexandrescu и т. д., могут повысить удобство использования, удобочитаемость и повысить эффективность инструментов, включая IDE)
Стандарты кодирования C ++ - Саттер и Александреску - изучены минусы
Пункт 35 из Стандартов кодирования C ++ перечисляет проблемы со сценарием получения из std::string
. Что касается сценариев, хорошо, что он иллюстрирует бремя предоставления большого, но полезного API, но хорошо и плохо, поскольку базовый API удивительно стабилен - будучи частью Стандартной библиотеки. Стабильная база - это обычная ситуация, но не более распространенная, чем нестабильная, и хороший анализ должен относиться к обоим случаям. Рассматривая список вопросов в книге, я специально сопоставлю применимость этих вопросов со словами:
а) class Issue_Id : public std::string { ...handy stuff... };
<- общественное происхождение, наше противоречивое использование <br>
б) class Issue_Id : public string_with_virtual_destructor { ...handy stuff... };
<- безопасный OO-вывод <br>
в) class Issue_Id { public: ...handy stuff... private: std::string id_; };
<- композиционный подход <br>
d) использование std::string
везде, с автономными функциями поддержки
(Надеемся, мы можем согласиться с тем, что состав является приемлемой практикой, поскольку он обеспечивает инкапсуляцию, безопасность типов, а также потенциально обогащенный API сверх std::string
.)
Итак, скажем, вы пишете какой-то новый код и начинаете думать о концептуальных объектах в ОО-смысле. Может быть, в системе отслеживания ошибок (я думаю о JIRA), один из них, скажем, Issue_Id. Содержимое данных - текстовое, состоящее из буквенного идентификатора проекта, дефиса и возрастающего номера вопроса: например, "MYAPP-1234". Идентификаторы проблем могут храниться в std::string
, и для идентификаторов проблем потребуется множество непростых текстовых поисков и операций манипуляции - большое подмножество тех, которые уже предоставлены в std::string
, и еще несколько для хорошей меры (например, получение компонент id проекта, предоставляющий следующий возможный идентификатор проблемы (MYAPP-1235)).
В список вопросов Саттера и Александреску ...
Функции, не являющиеся членами, хорошо работают в существующем коде, который уже манипулирует string
s. Если вместо этого вы предоставляете super_string
, вы заставляете изменения через базу кода изменять типы и сигнатуры функций на super_string
.
Фундаментальная ошибка с этим утверждением (и большинством из приведенных ниже) заключается в том, что оно способствует удобству использования только нескольких типов, игнорируя преимущества безопасности типов. Это выражает предпочтение d) выше, а не понимание c) или b) как альтернативы a). Искусство программирования включает в себя балансирование плюсов и минусов различных типов для достижения разумного повторного использования, производительности, удобства и безопасности. В нижеследующих пунктах подробно говорится об этом.
Используя общедоступную деривацию, существующий код может неявно обращаться к базовому классу string
как string
и продолжать вести себя, как всегда.Нет особой причины полагать, что существующий код захочет использовать какие-либо дополнительные функции из super_string
(в нашем случае Issue_Id) ... на самом деле это часто код поддержки более низкого уровня, уже существующий в приложении, для которого вы создаетеsuper_string
, и, следовательно, не обращая внимания на потребности, предусмотренные расширенными функциями.Например, скажем, есть функция, не являющаяся членом to_upper(std::string&, std::string::size_type from, std::string::size_type to)
- она все еще может быть применена к Issue_Id
.
Таким образом, если функция поддержки, не являющаяся членом, очищается или расширяется за преднамеренную стоимостьплотно связывая его с новым кодом, тогда его не нужно трогать.Если это , то пересматривается для поддержки идентификаторов проблем (например, использование понимания формата содержимого данных для ввода только букв алфавита в верхнем регистре), тогда, вероятно, было бы хорошо убедиться, что оно действительно передается.Issue_Id
, создавая перегрузку to_upper(Issue_Id&)
и придерживаясь либо деривационного, либо композиционного подходов, обеспечивающих безопасность типов.Независимо от того, используется ли super_string
или композиция, не имеет значения усилие или ремонтопригодность.to_upper_leading_alpha_only(std::string&)
многократно используемая отдельно стоящая функция поддержки вряд ли будет полезна - я не могу вспомнить, когда в последний раз я хотел такую функцию.
Импульс для использования std::string
везде нет 'Это качественно отличается от принятия всех ваших аргументов в качестве контейнеров вариантов или void*
s, так что вам не нужно изменять свои интерфейсы для принятия произвольных данных, но это делает для реализации подверженную ошибкам реализацию и меньше самодокументируемого и проверяемого компилятором кода.
Интерфейсные функции, которые принимают строку, теперь должны: a) держаться подальше от добавленной функциональности super_string
(бесполезно);б) скопировать их аргумент в super_string (расточительно);или c) приведите строковую ссылку к ссылке super_string (неудобной и потенциально недопустимой).
Кажется, это повторное обращение к первому пункту - старый код, который необходимо реорганизовать для использования новой функциональности, хотя ина этот раз код клиента, а не код поддержки.Если функция хочет начать обрабатывать свой аргумент как сущность , для которой актуальны новые операции, то она должна начать принимать свои аргументы как этот тип, и клиенты должны сгенерировать их и принятьони используют этот тип.Точно такие же проблемы существуют для композиции.В противном случае, c)
может быть практичным и безопасным, если будут следовать указаниям, которые я перечислю ниже, хотя это и ужасно.
Функции-члены super_string не имеют больше доступа к внутренним строкам строки, чем функции, не являющиеся членами, потому чтоСтрока, вероятно, не имеет защищенных членов (помните, что она не должна была быть производной)
Верно, но иногда это хорошо.Многие базовые классы не имеют защищенных данных.Открытый интерфейс string
- это все, что необходимо для манипулирования содержимым, и полезные функции (например, get_project_id()
, постулированные выше) могут быть элегантно выражены в терминах этих операций.Концептуально, много раз я извлекал из стандартных контейнеров, я не хотел расширять или настраивать их функциональность в соответствии с существующими линиями - они уже «идеальные» контейнеры - скорее я хотел добавить другое измерение поведения, которое является специфическимк моему заявлению, и не требует личного доступа.Это потому, что они уже являются хорошими контейнерами, и их хорошо использовать повторно.
Если super_string
скрывает некоторые функции string
(и переопределение невиртуальной функции в производном классе непереопределение, это просто сокрытие), что может вызвать широкую путаницу в коде, который манипулирует string
s, которые начали свою жизнь, автоматически преобразованными из super_string
s.
Верно и для композиции - и более вероятно, что это произойдет, поскольку код по умолчанию не пропускает что-либо и, следовательно, остается в синхронизации, а также верно в некоторых ситуациях с полиморфными иерархиями времени выполнения.Функции с одинаковыми именами, которые ведут себя по-разному в классах, которые изначально кажутся взаимозаменяемыми - просто неприятно.Фактически это обычное предостережение для правильного программирования ОО и опять же не является достаточной причиной для отказа от преимуществ в безопасности типов и т. Д.
Что если super_string
хочет унаследовать от string
для добавленияподробнее состояние [объяснение среза]
Согласовано - не очень хорошая ситуация, и где-то лично я склонен проводить черту, поскольку это часто перемещает проблемы удаления через указатель наоснование от области теории до очень практического - деструкторы не вызываются для дополнительных членов.Тем не менее, нарезка часто может делать то, что нужно - учитывая подход получения super_string
не для того, чтобы изменить свою унаследованную функциональность, но чтобы добавить другое «измерение» функциональности, специфичной для приложения ....
По общему признаниюУтомительно, когда приходится писать сквозные функции для тех функций-членов, которые вы хотите сохранить, но такая реализация намного лучше и безопаснее, чем использование открытого или непубличного наследования.
Ну, конечно, согласен с утомительным....
Рекомендации по успешной деривации без виртуального деструктора
- в идеале, избегайте добавления элементов данных в производный класс: варианты нарезки могут случайно удалить элементы данных, повредить их, не удастсяинициализируйте их ...
- еще больше - избегайте членов данных, не относящихся к POD: удаление с помощью указателя базового класса в любом случае является технически неопределенным поведением, но если типы, не являющиеся POD, не могут запускать свои деструкторы, то с большей вероятностьюне теоретические проблемы с утечками ресурсов, плохая справочная информацияnts и т. д.
- соблюдайте принципал подстановки Лискова / вы не можете надежно поддерживать новые инварианты
- , например, при выводе из
std::string
вы не можете перехватить несколько функций и ожидать ваших объектовоставаться в верхнем регистре: любой код, который обращается к ним через std::string&
или ...*
, может использовать исходные реализации функций std::string
для изменения значения) - наследовать для моделирования объекта более высокого уровня в вашем приложении,расширить унаследованную функциональность некоторыми функциями, которые используют, но не конфликтуют с базой;не ожидайте и не пытайтесь изменить базовые операции - и доступ к этим операциям - предоставляемый базовым типом
- , помните о связи: базовый класс нельзя удалить без влияния на клиентакодировать, даже если базовый класс развивается, чтобы иметь несоответствующую функциональность, то есть удобство использования вашего производного класса зависит от постоянной пригодности базового
- , иногда даже если вы используете композицию, вам нужно будет выставить элемент данных из-за производительности,проблемы безопасности потоков или отсутствие семантики значений - так что потеря инкапсуляции от публичного деривации не будет ощутимо хуже
- тем более вероятно, что люди, использующие потенциально производный класс, не будут знать о его реализациикомпромиссы, тем меньше вы можете позволить себе сделать их опасными
- , поэтому низкоуровневые широко развернутые библиотеки со многими случайными пользователями должны быть более осторожны с опасным происхождением, чем локализованное использование программистами, регулярно использующими функциональность в приложении.Уровень и / или в «частной» реализации / библиотеки
Сводка
Такой вывод не без проблем, поэтому не рассматривайте его, если не конечный результатоправдывает средства.Тем не менее, я категорически отвергаю любые утверждения, что это не может использоваться безопасно и надлежащим образом в конкретных случаях - это просто вопрос, где провести черту.
Личный опыт
Иногда я получаю от std::map<>
, std::vector<>
, std::string
и т. Д. - меня никогда не сжигали проблемы с нарезкой или удалением через базовый класс-указатель, и я сохранил много время и энергия для более важных вещей. Я не храню такие объекты в разнородных полиморфных контейнерах. Но вам нужно подумать, все ли программисты, использующие этот объект, знают о проблемах и, вероятно, будут программировать соответственно. Мне лично нравится писать свой код для использования полиморфизма кучи и времени выполнения только тогда, когда это необходимо, в то время как некоторые люди (из-за фонов Java, их предпочтительного подхода к управлению зависимостями перекомпиляции или переключению между поведением во время выполнения, средствами тестирования и т. Д.) Обычно используют их и поэтому нужно больше заботиться о безопасных операциях с помощью указателей базового класса.