Почему не следует выводить из строкового класса c ++ std? - PullRequest
60 голосов
/ 15 мая 2011

Я хотел бы спросить о конкретном замечании, сделанном в Effective C ++.

Там написано:

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

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

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

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

Ответы [ 8 ]

55 голосов
/ 15 мая 2011

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

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

В идиоматическом C ++ есть два варианта использования класса:

  • private наследование, используемое дляМиксины и аспектно-ориентированное программирование с использованием шаблонов.
  • public наследование, используется только для полиморфных ситуаций . EDIT : Хорошо, я думаю, это можно использовать и в нескольких смешанных сценариях, таких как boost::iterator_facade, которые появляются, когда используется CRTP .

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

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

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

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 и C # это не проблема, поскольку единственное, что касается автоматического хранения, - это ссылочные типы, и ссылки всегда имеют одинаковый размер.В C ++ это не так.

Короче говоря, не используйте наследование для привязки методов в C ++.Это не идиоматично и приводит к проблемам с языком.По возможности используйте функции, не являющиеся друзьями, не являющиеся членами, а затем композицию.Не используйте наследование, если вы не являетесь метапрограммирующим шаблоном или не хотите полиморфного поведения.Для получения дополнительной информации см. Effective C ++ Скотта Мейерса, пункт 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);
}
24 голосов
/ 16 мая 2011

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

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

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

  • вам нужны некоторые преимущества безопасности типов и читаемости кода, предоставляемые выделенными пользовательскими типами (классами)
  • существующая база идеально подходит для хранения данных и допускает низкоуровневые операции, которые клиентский код также хотел бы использовать
  • , вам нужно удобство повторного использования функций, поддерживающих этот базовый класс
  • вы понимаете, что любые дополнительные инварианты, в которых ваши данные логически необходимы, могут быть реализованы только в коде, явно обращающемся к данным как производному типу, и в зависимости от того, в какой степени это "естественно" произойдет в вашем проекте, и от того, насколько вы можете доверятьклиентский код, чтобы понять и сотрудничать с логически-идеейВо всех инвариантах вы можете захотеть, чтобы функции-члены производного класса переоценивали ожидания (и выбрасывали, или что-то в этом роде)
  • производный класс добавляет некоторые специфичные для типа удобные функции, работающие с данными, такие как пользовательский поиск, фильтрация данных/ модификации, потоковая передача, статистический анализ, (альтернативные) итераторы
  • связывание клиентского кода с базой более уместно, чем связывание с производным классом (поскольку база либо стабильна, либо изменения в ней отражают также улучшения функциональностиядро к производному классу)
    • Другими словами: вы хотите, чтобы производный класс продолжал предоставлять тот же 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, их предпочтительного подхода к управлению зависимостями перекомпиляции или переключению между поведением во время выполнения, средствами тестирования и т. Д.) Обычно используют их и поэтому нужно больше заботиться о безопасных операциях с помощью указателей базового класса.

10 голосов
/ 15 мая 2011

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

Тогда зачем вам это наследовать?

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

9 голосов
/ 15 мая 2011

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

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

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

4 голосов
/ 15 мая 2011

Почему не следует выводить из строкового класса c ++ 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(); }
//...
};
2 голосов
/ 15 мая 2011

Существуют две простые причины, по которым не следует выводить из неполиморфного класса:

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

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

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

РЕДАКТИРОВАТЬ :

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

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

0 голосов
/ 15 июля 2018

Как только вы добавите какой-либо член (переменную) в свой производный класс std :: string, будете ли вы систематически завинчивать стек, если попытаетесь использовать положительные элементы std с экземпляром вашего производного класса std :: string? Поскольку функции / члены stdc ++ имеют свои указатели стека [индексы], фиксированные [и скорректированные] по размеру / границе размера экземпляра (base std :: string).

правый

Пожалуйста, поправьте меня, если я ошибаюсь.

0 голосов
/ 15 мая 2011

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

C ++ стандартный раздел 5.3.5 / 3:

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...