Расширение стандартной библиотеки C ++ путем наследования?

Принято считать, что стандартная библиотека C ++ обычно не предназначена для расширения с использованием наследования. Конечно, я (и другие) критиковал людей, предлагающих уйти из таких классов, как std::vector. Однако этот вопрос: исключения c ++, может ли what () быть NULL? сделано Я понимаю, что есть по крайней мере одна часть стандартной библиотеки, которая предназначена для такого расширения - std::exception.

Итак, мой вопрос состоит из двух частей:

  1. Существуют ли какие-либо другие классы стандартной библиотеки, производные от которых?

  2. Если кто-то является производным от класса стандартной библиотеки, такого как std::exception, привязан ли он к интерфейсу, описанному в стандарте ISO? Например, будет ли программа, в которой используется класс исключения, функция-член what(), не возвращающая NTBS (например, возвращающий нулевой указатель), соответствовать стандарту?


person Community    schedule 02.07.2009    source источник


Ответы (10)


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

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

  • Если у него нет virtual методов, вы не должны использовать его в качестве основы. Это исключает std::vector и тому подобное.
  • Если у него есть virtual методов, то он является кандидатом для использования в качестве базового класса.
  • Если вокруг много friend операторов, держитесь подальше, поскольку, вероятно, возникла проблема с инкапсуляцией.
  • Если это шаблон, присмотритесь к нему, прежде чем наследовать от него, поскольку вместо этого вы, вероятно, можете настроить его с помощью специализаций.
  • Наличие механизма, основанного на политике (например, std::char_traits), является хорошим признаком того, что вы не должны использовать его в качестве основы.

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

(б) Я бы применил здесь LSP. Если кто-то вызывает what() для вашего исключения, то его наблюдаемое поведение должно соответствовать поведению std::exception. Я не думаю, что это действительно проблема соответствия стандартам, а скорее проблема правильности. Стандарт не требует, чтобы подклассы заменяли базовые классы. На самом деле это всего лишь "передовой опыт".

person D.Shawley    schedule 02.07.2009
comment
Вы упомянули об отказе от наследования от классов, у которых нет виртуальных членов, но затем вы также упомянули классы политик. Итак (например), что не так с наследованием от распределителя (частным образом)? - person Konrad Rudolph; 03.07.2009
comment
Я бы добавил, что некоторые вещи должны иметь небольшую степень расширения через наследование, например std::stack и std::queue, потому что они защищены, поскольку единственная причина для защиты вещей - это разрешить дочернему классу читать данные. Очевидно, вам нужно быть очень осторожным с тем, что вы с этим делаете. - person Evan Teran; 06.03.2010

а) потоковая библиотека сделана для наследования :)

person AraK    schedule 02.07.2009

Что касается вашей части b, из 17.3.1.2 «Требования», абзац 1:

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

  • Аргументы шаблона
  • Производные классы
  • Контейнеры, итераторы и / или алгоритмы, соответствующие соглашению об интерфейсе

Хотя 17.3 является информативным, а не обязательным, намерение комитета в отношении поведения производного класса ясно.

Для других очень похожих точек расширения существуют четкие требования:

  • 17.1.15 «требуемое поведение» охватывает замену (оператор new и т. Д.) И функции обработчика (обработчики завершения и т. Д.) И отбрасывает все несовместимое поведение в UB-область.
  • 17.4.3.6/1: «В определенных случаях (функции замены, функции обработчика, операции с типами, используемые для создания экземпляров компонентов шаблона стандартной библиотеки), стандартная библиотека C ++ зависит от компонентов, поставляемых программой C ++. Если эти компоненты не соответствуют их требованиям , Стандарт не предъявляет требований к реализации ».

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

person Community    schedule 17.01.2010

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

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

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

std::exception - еще один пример.

И, как сказал Д.Шоули, я бы применил LSP к вашему второму вопросу. . Всегда должна быть разрешена замена базового класса производным. Если я вызываю exception::what(), он должен следовать контракту, указанному классом exception, независимо от того, откуда появился объект exception, или действительно ли это производный класс, преобразованный с повышением. И в этом случае этот контракт является стандартным обещанием вернуть NTBS. Если вы заставили производный класс вести себя иначе, вы нарушите стандарт, потому что объект типа std::exception больше не возвращает NTBS.

person jalf    schedule 02.07.2009

Чтобы ответить на вопрос 2):

Я считаю, что да, они будут связаны описанием интерфейса стандарта ISO. Например, стандарт позволяет переопределить operator new и operator delete глобально. Однако стандарт гарантирует, что operator delete не будет работать с нулевыми указателями.

Несоблюдение этого, безусловно, неопределенное поведение (по крайней мере, для Скотта Майерса). Думаю, можно сказать, что то же самое верно по аналогии для других областей стандартной библиотеки.

person Konrad Rudolph    schedule 02.07.2009

Некоторые элементы в functional, такие как greater<>, less<> и mem_fun_t, являются производными от unary_operator<> и binary_operator<>. Но, IIRC, это дает вам только некоторые typedef.

person eduffy    schedule 02.07.2009

Экономное правило: «Любой класс может использоваться в качестве базового класса; ответственность за его безопасное использование при отсутствии виртуальных методов, включая виртуальный деструктор, полностью принадлежит производному автору». Добавление члена, не являющегося POD, в дочерний элемент std :: exception - такая же ошибка пользователя, как и в производном классе std :: vector. Идея о том, что контейнеры не «предназначены» для использования в качестве базовых классов, является инженерным примером того, что профессора литературы называют «Заблуждение авторского намерения».

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

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

person Thomas L Holaday    schedule 03.07.2009

Думаю, что на второй вопрос ответ положительный. Стандарт говорит, что член what std :: exception должен возвращать значение, отличное от NULL. Не имеет значения, есть ли у меня значение стека, ссылки или указателя на std :: exception. Возврат what () ограничен стандартом.

Конечно, можно вернуть NULL. Но я бы счел такой класс нестандартным.

person JaredPar    schedule 02.07.2009

Я знаю, что это старый вопрос, но я хотел бы добавить сюда свой комментарий.

Уже несколько лет я использую class CfgValue, который наследуется от std :: string, хотя документация (или какая-то книга или какой-то стандартный документ, у меня сейчас нет источника под рукой) говорит, что пользователи не должны наследовать от std :: string . И этот класс содержит комментарий «TODO: удалить наследование из std :: string» с годами.

Класс CfgValue просто добавляет некоторые конструкторы, сеттеры и геттеры для быстрого преобразования между строками и числовыми и логическими значениями, а также преобразование из кодировки utf8 (хранится в std :: string) в кодировку ucs2 (хранится в std :: wstring) и так далее.

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

class CfgValue : public std::string {
public:
    ...
    CfgValue( const int i ) : std::string() { SetInteger(i); }
    ...
    void SetInteger( int i );
    ...
    int GetInteger() const;
    ...
    operator std::wstring() { return utf8_to_ucs16(*this); }
    operator std::wstring() const { return utf8_to_ucs16(*this); }
    ...
};
person Frunsi    schedule 13.12.2009

w.r.t вопрос 2), в соответствии со стандартом C ++, производный класс исключения должен указывать не-throw, т.е. спецификацию throw (), а также возвращать ненулевое значение. Во многих случаях это означает, что производный класс исключения не должен использовать std :: string, так как std :: string сам может генерировать в зависимости от реализации.

person Abhay    schedule 02.07.2009