Подтвердить что ты не робот

Почему не следует выводить из С++ std string class?

Я хотел спросить о конкретном моменте, сделанном в Effective С++.

В нем говорится:

Деструктор должен быть виртуальным, если класс должен действовать как полиморфный класс. Далее добавляется, что, поскольку std::string не имеет виртуального деструктора, его никогда не следует выводить. Кроме того, std::string даже не предназначен для базового класса, забудьте полиморфный базовый класс.

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

Является единственной причиной, по которой я не должен выходить из класса std::string, у нее нет виртуального деструктора? Для целей повторного использования базовый класс может быть определен, и из него может наследоваться несколько производных классов. Итак, что делает std::string не приемлемым как базовый класс?

Кроме того, если существует базовый класс, чисто определенный для цели многократного использования, и существует много производных типов, есть ли способ предотвратить выполнение клиентом Base* p = new Derived(), потому что классы не предназначены для использования полиморфно?

4b9b3361

Ответ 1

Я думаю, что это утверждение отражает путаницу здесь (внимание мое):

Я не понимаю, что конкретно требуется в классе, чтобы иметь право быть базовым классом (не полиморфным)?

В идиоматическом С++ существует два использования для вывода из класса:

  • private наследование, используемое для микшинов и аспектно-ориентированное программирование с использованием шаблонов.
  • public наследование, используемое только для полиморфных ситуаций. РЕДАКТИРОВАТЬ. Хорошо, я думаю, это можно было бы использовать и в нескольких сценариях mixin - например, boost::iterator_facade - которые появляются, когда CRTP.

Нет абсолютно никаких оснований публично выводить класс на С++, если вы не пытаетесь сделать что-то полиморфное. Язык поставляется со свободными функциями в качестве стандартной функции языка, а бесплатные функции - это то, что вы должны использовать здесь.

Подумайте об этом так: действительно ли вы хотите заставить клиентов вашего кода конвертировать в какой-то проприетарный класс строк просто потому, что вы хотите использовать несколько методов? Поскольку в отличие от Java или С# (или большинства аналогичных объектно-ориентированных языков), когда вы выводите класс на С++, большинство пользователей базового класса должны знать об этом изменении. В Java/С# классы обычно получают доступ через ссылки, похожие на указатели С++. Таким образом, существует уровень косвенности, который отделяет клиентов вашего класса, позволяя вам заменить производный класс, не зная других клиентов.

Однако в С++ классы значения типов - в отличие от большинства других языков OO. Самый простой способ увидеть это так называемая проблема среза. В основном рассмотрим:

int StringToNumber(std::string copyMeByValue)
{
    std::istringstream converter(copyMeByValue);
    int result;
    if (converter >> result)
    {
        return result;
    }
    throw std::logic_error("That is not a number.");
}

Если вы передадите свою собственную строку этому методу, конструктор копирования для std::string будет вызван, чтобы сделать копию, не конструктор копирования для вашего производного объекта - независимо от того, какой дочерний класс из std::string. Это может привести к несогласованности между вашими методами и привязанностью к строке. Функция StringToNumber не может просто взять любой ваш производный объект и скопировать его просто потому, что ваш производный объект, вероятно, имеет другой размер, чем std::string, но эта функция была скомпилирована, чтобы зарезервировать только пространство для std::string в автоматическое хранение. В Java и С# это не проблема, потому что единственное, что связано с автоматическим хранением, это ссылочные типы, а ссылки всегда одного размера. Не так в С++.

Короче говоря - не используйте наследование для применения методов на С++. Это не идиоматично и приводит к проблемам с языком. Используйте, если это возможно, функции, отличные от друга, не являющиеся членами, за которыми следует композиция. Не используйте наследование, если вы не метапрограммируете шаблон или не хотите полиморфного поведения. Для получения дополнительной информации см. " Эффективный С++. Пункт 23: Предпочитайте функции, не являющиеся членами-не-членами, для функций-членов.

EDIT: Здесь приведен более полный пример, показывающий проблему среза. Вы можете увидеть его вывод на codepad.org

#include <ostream>
#include <iomanip>

struct Base
{
    int aMemberForASize;
    Base() { std::cout << "Constructing a base." << std::endl; }
    Base(const Base&) { std::cout << "Copying a base." << std::endl; }
    ~Base() { std::cout << "Destroying a base." << std::endl; }
};

struct Derived : public Base
{
    int aMemberThatMakesMeBiggerThanBase;
    Derived() { std::cout << "Constructing a derived." << std::endl; }
    Derived(const Derived&) : Base() { std::cout << "Copying a derived." << std::endl; }
    ~Derived() { std::cout << "Destroying a derived." << std::endl; }
};

int SomeThirdPartyMethod(Base /* SomeBase */)
{
    return 42;
}

int main()
{
    Derived derivedObject;
    {
        //Scope to show the copy behavior of copying a derived.
        Derived aCopy(derivedObject);
    }
    SomeThirdPartyMethod(derivedObject);
}

Ответ 2

Предлагать стороне счетчика общей рекомендации (которая звучит, когда нет конкретных проблем/проблем с производительностью)...

Сценарий для разумного использования

Существует, по крайней мере, один сценарий, в котором общественный вывод из баз без виртуальных деструкторов может быть хорошим решением:

  • вы хотите, чтобы некоторые из преимуществ безопасности типов и кода были предоставлены специализированными пользовательскими типами (классами)
  • существующая база идеально подходит для хранения данных и позволяет выполнять операции низкого уровня, которые клиентский код также хотел бы использовать
  • вы хотите упростить повторное использование функций, поддерживающих этот базовый класс
  • вы понимаете, что любые любые дополнительные инварианты, которые логически необходимы вашим данным, могут быть введены только в том случае, когда код явно обращается к данным как производный тип и зависит от степени, в которой это будет "естественно" в вашем дизайне, и сколько вы может доверять клиентскому коду, чтобы понимать и сотрудничать с логически-идеальными инвариантами, вы можете хотеть, чтобы функции членов производного класса повторяли ожидания (и бросали или что-то еще)
  • производный класс добавляет некоторые высокоуровневые функции удобства, работающие над данными, такие как пользовательские запросы, фильтрация/модификация данных, потоковая передача, статистический анализ, (альтернативные) итераторы.
  • Связывание клиентского кода с базой более уместно, чем соединение с производным классом (поскольку база либо стабильна, либо изменения в ней отражают улучшения функциональности, также являющиеся основой для производного класса)
    • по-другому: вы хотите, чтобы производный класс продолжал раскрывать тот же API, что и базовый класс, даже если это означает, что клиентский код вынужден изменять, а не изолировать его каким-то образом, что позволяет базовый и производный API вырасти из синхронизации
  • вы не собираетесь смешивать указатели на базовые и производные объекты в частях кода, ответственных за их удаление.

Это может показаться довольно ограничительным, но в реальных программах, соответствующих этому сценарию, есть много случаев.

Фоновая дискуссия: относительные достоинства

Программирование - это компромиссы. Прежде чем писать более концептуально "правильную" программу:

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

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

Причины рассмотрения деривации без виртуального деструктора

Скажем, что у вас есть класс D, публично полученный из B. Без усилий операции на B возможны на D (за исключением строительства, но даже если есть много конструкторов, вы часто можете обеспечить эффективную пересылку, один шаблон для каждого отдельного числа аргументов конструктора: например, template <typename T1, typename T2> D(const T1& x1, const T2& t2) : B(t1, t2) { }. Лучшее обобщенное решение в вариативных шаблонах С++ 0x.)

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

Перефразируя это: сокращается явная связь между базовым и производным классами, но увеличивается связь между базой и клиентом.

Это часто НЕ то, что вы хотите, но иногда оно идеально, а иногда и без проблем (см. следующий параграф). Изменения в основной силе приводят к изменению кода клиента в местах, распределенных по всей базе кода, и иногда люди, меняющие базу, могут даже не иметь доступа к клиентскому коду для просмотра или обновления соответственно. Иногда бывает лучше: если вы как поставщик производного класса - "человек посередине" - хотите, чтобы изменения базового класса передавались клиентам, и вы обычно хотите, чтобы клиенты могли - иногда принудительно - обновлять свой код, когда изменения базового класса без необходимости постоянного участия, тогда общественный вывод может быть идеальным. Это обычное явление, когда ваш класс не является самостоятельной независимой сущностью, а тонким значением, добавляемым к базе.

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

В общем, публичный вывод - это быстрый способ получить или приблизиться к идеальному знакомому интерфейсу базового класса для производного класса - таким образом, чтобы он был кратким и, по-видимому, правильным для кодера-хранителя и клиента, с дополнительными функциональными возможностями, доступными как (IMHO), который, очевидно, отличается от Sutter, Alexandrescu и т.д., может помочь в удобстве использования, удобочитаемости и помощи инструментам повышения производительности, включая IDE).

Стандарты кодирования С++ - рассмотрены Sutter & Alexandrescu - cons

Пункт 35 Стандартов кодирования С++ перечисляет проблемы со сценарием из std::string. По мере того, как сценарии идут, хорошо, что это иллюстрирует бремя раскрытия большого, но полезного API, но и хорошее, и плохое, поскольку базовый API замечательно стабилен - является частью стандартной библиотеки. Устойчивая база - обычная ситуация, но не более распространенная, чем летучая, и хороший анализ должен относиться к обоим случаям. Рассматривая список книг по этим вопросам, я специально противопоставил бы применимость этих проблем к случаям:

а) class Issue_Id : public std::string { ...handy stuff... }; < - общественное происхождение, наше противоречивое использование
b) class Issue_Id : public string_with_virtual_destructor { ...handy stuff... }; < - более безопасный вывод OO
c) class Issue_Id { public: ...handy stuff... private: std::string id_; }; < - композиционный подход
d) используя std::string всюду, с автономными вспомогательными функциями

(Надеемся, мы сможем согласиться, что композиция является приемлемой практикой, поскольку она обеспечивает инкапсуляцию, безопасность типов, а также потенциально обогащенный API и выше, чем std::string.)

Итак, скажем, вы пишете новый код и начинаете думать о концептуальных сущностях в смысле OO. Возможно, в системе отслеживания ошибок (я думаю об JIRA) один из них говорит Issue_Id. Содержимое данных является текстовым, состоящим из идентификатора буквенного проекта, дефиса и возрастающего номера проблемы: например. "MYAPP-1234". Идентификаторы проблем могут быть сохранены в std::string, и будет очень мало текстовых запросов и операций манипуляции, необходимых для идентификаторов проблем - большого подмножества тех, которые уже были предоставлены на std::string, и еще нескольких для хорошей меры (например, получение компонент id проекта, обеспечивающий следующий возможный идентификатор проблемы (MYAPP-1235)).

На Саттера и Александреску список проблем...

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

Основная ошибка с этим требованием (и большинство из приведенных ниже) заключается в том, что она способствует удобству использования только нескольких типов, игнорируя преимущества безопасности типов. Он выражает предпочтение г) выше, а не понимание в) или б) в качестве альтернативы а). Искусство программирования предполагает балансирование плюсов и минусов различных типов для обеспечения разумного повторного использования, производительности, удобства и безопасности. В нижеследующих пунктах подробно описывается это.

Используя общепринятый вывод, существующий код может неявно получить доступ к базовому классу 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, создавая перегрузку ala to_upper(Issue_Id&) и придерживаться либо подходов к выведению, либо композиционных подходов, обеспечивающих безопасность типа. Используется ли super_string или композиция, не имеет никакого значения для усилий или ремонтопригодности. A to_upper_leading_alpha_only(std::string&) многоразовая автономная функция поддержки вряд ли будет очень полезна - я не могу вспомнить последний раз, когда мне нужна такая функция.

Импульс использовать std::string везде не качественно отличается от того, чтобы принимать все ваши аргументы как контейнеры вариантов или void*, поэтому вам не нужно менять свои интерфейсы для принятия произвольных данных, но это делает причиной ошибок реализации и менее самодокументирующегося и контролируемого компилятором кода.

Функции интерфейса, которые берут строку, теперь должны: a) держаться подальше от super_string добавленной функциональности (не полезной); б) копировать свой аргумент в супер_строку (расточительно); или c) привести ссылку на строку к ссылке super_string (неудобно и потенциально незаконно).

Кажется, что он пересматривает первый точечный код, который необходимо реорганизовать для использования новой функциональности, хотя на этот раз клиентский код, а не код поддержки. Если функция хочет начать рассматривать свой аргумент как объект, для которого важны новые операции, тогда он должен начать принимать свои аргументы как этот тип, и клиенты должны сгенерировать их и принять их с использованием этого типа. Точно такие же проблемы существуют для композиции. В противном случае, c) может быть практичным и безопасным, если следовать приведенным ниже рекомендациям, хотя это и уродливо.

Функции члена super_string больше не имеют доступа к строковым внутренним функциям, чем функции, отличные от функций, поскольку строка, вероятно, не имеет защищенных членов (помните, что она не должна была быть получена изначально)

Правда, но иногда это хорошо. У многих базовых классов нет защищенных данных. Общественный string интерфейс - это все, что необходимо для управления содержимым, и полезная функциональность (например, get_project_id(), постулированная выше) может быть элегантно выражена в терминах этих операций. Понятно, что много раз я получал из стандартных контейнеров, я не хотел расширять или настраивать их функциональность вдоль существующих линий - они уже являются "идеальными" контейнерами - скорее я хотел добавить еще одно измерение поведения, которое к моему приложению и не требует личного доступа. Это потому, что они уже являются хорошими контейнерами, которые можно использовать повторно.

Если super_string скрывает некоторые из функций string (и переопределение невиртуальной функции в производном классе не является переопределяющим, оно просто скрывается), что может вызвать повсеместное замешательство в коде, который манипулирует string, который начал свою жизнь преобразован автоматически от super_string s.

Истина для композиции тоже - и, скорее всего, произойдет, поскольку код не по умолчанию не передаёт вещи и, следовательно, остается в синхронизации, а также истина в некоторых ситуациях с полиморфными иерархиями во время выполнения. Самед назвал функции, которые ведут себя по-разному в классах, которые изначально выглядят взаимозаменяемыми - просто противно. Это является фактически обычной осторожностью для правильного программирования OO, и снова не является достаточной причиной для отказа от преимуществ в безопасности типов и т.д.

Что делать, если super_string хочет наследовать от string, чтобы добавить больше состояний [объяснение разрезания]

Согласовано - не хорошая ситуация, и где-то я лично склонен рисовать линию, поскольку она часто перемещает проблемы удаления с помощью указателя на базу от теории теории до очень практических - деструкторы не используются для дополнительных членов, Тем не менее, нарезка часто может делать то, что нужно - учитывая подход получения super_string, чтобы не изменять его унаследованную функциональность, а добавить еще одно "измерение" функциональности приложения...

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

Ну, конечно, согласитесь с скукой....

Рекомендации по успешному деривации без виртуального деструктора

  • В идеале, избегайте добавления членов данных в производный класс: варианты разрезания могут случайно удалить члены данных, испортить их, не инициализировать их...
  • тем более - избегать элементов данных, отличных от POD: удаление с помощью указателя базового класса технически undefined поведение в любом случае, но с не-POD-типами, неспособными запустить их деструкторы, более вероятно, будут иметь нетеоретические проблемы с ресурсом утечки, неправильные подсчеты ссылок и т.д.
  • Почетный руководитель Liskov Substitution/вы не можете надежно поддерживать новые инварианты
    • Например, при выводе std::string вы не можете перехватить несколько функций и ожидать, что ваши объекты останутся прописными: любой код, который обращается к ним через std::string& или ...*, может использовать std::string реализацию исходной функции для изменения значения)
    • получить модель более высокого уровня в вашем приложении, расширить унаследованные функциональные возможности с помощью некоторой функциональности, которая использует, но не конфликтует с базой; не ожидайте или не пытайтесь изменить основные операции - и доступ к этим операциям - предоставляется базовым типом
  • знать о связи: базовый класс нельзя удалить, не затрагивая клиентский код, даже если базовый класс развивается, чтобы иметь ненадлежащую функциональность, т.е. удобство использования вашего производного класса зависит от текущей уместности базы
    • иногда, даже если вы используете композицию, вам нужно будет выставить элемент данных из-за производительности, проблем с безопасностью потоков или отсутствия семантики значений, поэтому потеря инкапсуляции из общедоступного вывода не ощутимо хуже.
  • Чем больше людей, использующих потенциально производный класс, не будут знать о компрометации его реализации, тем меньше вы можете позволить себе сделать их опасными
    • поэтому низкоуровневые широко развернутые библиотеки со многими случайными пользователями должны быть более осторожными в отношении опасного вывода, чем локализованное использование программистами, обычно использующих функциональность на уровне приложений и/или в "private" реализациях/библиотеках

Резюме

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

Персональный опыт

Я иногда берусь из std::map<>, std::vector<>, std::string и т.д. - Я никогда не сжигал проблемы с нарезкой или удалением по базовому классу, и я сохранил много времени и энергии для более важных вещей. Я не храню такие объекты в гетерогенных полиморфных контейнерах. Но вам нужно подумать о том, знают ли все программисты, использующие объект, проблемы и, вероятно, будут соответствующим образом программировать. Мне лично нравится писать свой код, чтобы использовать кучи и полиморфизм во время выполнения только тогда, когда это необходимо, в то время как некоторые люди (из-за фонов Java, их предпочтительного подхода к управлению зависимостями перекомпиляции или переключения между режимами выполнения, средствами тестирования и т.д.) Используют их обычно и поэтому нужно больше заботиться о безопасных операциях с помощью указателей базового класса.

Ответ 3

Не только деструктор не виртуальный, std::string не содержит виртуальных функций вообще, а не защищенных членов. Это очень затрудняет изменение производного класса.

Тогда почему бы вам извлечь из этого?

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

Ответ 4

Если вы действительно хотите извлечь из него (не обсуждая, почему вы хотите это сделать), я думаю, что вы можете предотвратить создание экземпляра непосредственного кучи класса Derived, сделав его operator new private:

class StringDerived : public std::string {
//...
private:
  static void* operator new(size_t size);
  static void operator delete(void *ptr);
}; 

Но таким образом вы ограничиваете себя любыми динамическими объектами StringDerived.

Ответ 5

Почему не следует выводить из строкового класса С++ std?

Потому что он не нужен. Если вы хотите использовать DerivedString для расширения функциональности; Я не вижу никаких проблем при получении std::string. Единственное, что вы не должны взаимодействовать между обоими классами (т.е. Не использовать string в качестве приемника для DerivedString).

Есть ли способ предотвратить выполнение клиентом Base* p = new Derived()

Да. Убедитесь, что вы предоставили inline обертки вокруг методов Base внутри класса Derived. например.

class Derived : protected Base { // 'protected' to avoid Base* p = new Derived
  const char* c_str () const { return Base::c_str(); }
//...
};

Ответ 6

Есть две простые причины, по которым не вытекает неполиморфный класс:

  • Технический: он вводит ошибки среза (потому что в С++ мы передаем значение, если не указано иное)
  • Функциональный: если он не является полиморфным, вы можете добиться того же эффекта с композицией и некоторой функцией переадресации

Если вы хотите добавить новые функции в std::string, сначала рассмотрите возможность использования свободных функций (возможно, шаблонов), например Boost String Algorithm библиотека.

Если вы хотите добавить новых членов данных, тогда правильно упакуйте доступ к классу, вставив его (состав) внутри класса вашего собственного дизайна.

ИЗМЕНИТЬ

@Тони правильно заметил, что причина, по которой я ссылалась, была, вероятно, бессмысленной для большинства людей. Существует простое эмпирическое правило, в хорошем дизайне, в котором говорится, что когда вы можете выбрать решение среди нескольких, вы должны рассмотреть тот, у которого слабая связь. Композиция имеет более слабую связь, которая наследуется и, следовательно, должна быть предпочтительной, когда это возможно.

Кроме того, композиция дает вам возможность красиво обернуть метод исходного класса. Это невозможно, если вы выбираете наследование (public), а методы не являются виртуальными (что здесь и есть).

Ответ 7

В стандарте С++ указано, что если деструктор класса Base не является виртуальным и вы удаляете объект базового класса, который указывает на объект производного класса, то он вызывает undefined Behavior.

Стандартная секция С++ 5.3.5/3:

если статический тип операнда отличается от его динамического типа, статический тип должен быть базовым классом динамического типа операндов, а статический тип должен иметь виртуальный деструктор или поведение undefined.

Чтобы быть понятным для Non-полиморфного класса и необходимости виртуального деструктора
Цель создания виртуального деструктора - облегчить полиморфное удаление объектов посредством удаления-выражения. Если нет полиморфного удаления объектов, вам не нужен виртуальный деструктор.

Почему бы не получить класс String?
Как правило, следует избегать получения какого-либо стандартного класса контейнеров из-за того, что у них нет виртуальных деструкторов, что делает невозможным удаление объектов полиморфно. Что касается класса string, класс string не имеет виртуальных функций, поэтому вы ничего не можете переопределить. Лучшее, что вы можете сделать, это скрыть что-то.

Если вы вообще хотите иметь такую ​​строку, как функциональность, вы должны написать собственный класс, а не наследовать от std::string.