Расширение стандартной библиотеки С++ по наследству?
Общеизвестно, что стандартная библиотека С++ обычно не предназначена для расширения с использованием наследования. Конечно, я (и другие) критиковал людей, которые предполагают получение таких классов, как std::vector
. Однако этот вопрос: исключения С++, может ли() быть NULL? заставило меня понять, что есть хотя бы одна часть стандартной библиотеки, которая должна быть настолько расширенной - std::exception
.
Итак, мой вопрос состоит из двух частей:
-
Существуют ли какие-либо другие классы стандартной библиотеки, которые должны быть получены из?
-
Если кто-либо из класса стандартной библиотеки, такого как std::exception
, связан с интерфейсом, описанным в стандарте ISO? Например, будет ли стандартная конфигурация программой, использующей класс исключений, кто не имеет функции-члена what()
, не возвращает NTBS (скажем, вернул нулевой указатель)?
Ответы
Ответ 1
Хороший хороший вопрос. Я действительно хочу, чтобы стандарт был немного более конкретным в отношении предполагаемого использования. Может быть, должен быть документ C Обоснование, который находится рядом с языковым стандартом. В любом случае, вот такой подход, который я использую:
(a) Я не знаю о существовании такого списка. Вместо этого я использую следующий список, чтобы определить, может ли тип стандартной библиотеки быть унаследован от:
- Если у него нет методов
virtual
, вы не должны использовать его в качестве базы. Это исключает std::vector
и т.п.
- Если у него есть методы
virtual
, то он является кандидатом на использование в качестве базового класса.
- Если существует множество операторов
friend
, плавающих вокруг, то избегайте их устранения, поскольку существует проблема с инкапсуляцией.
- Если это шаблон, то посмотрите ближе, прежде чем наследовать его, поскольку вы можете настроить его с помощью специализаций.
- Наличие механизма на основе политик (например,
std::char_traits
) - довольно хорошая подсказка, что вы не должны использовать его в качестве базы.
К сожалению, я не знаю хорошего всеобъемлющего или черно-белого списка. Обычно я чувствую себя как кишка.
(b) Я бы применил LSP здесь. Если кто-то звонит what()
в ваше исключение, то наблюдаемое поведение должно соответствовать значению std::exception
. Я не думаю, что это действительно вопрос соответствия стандартам, а также проблема правильности. Стандарт не требует, чтобы подклассы были подменяемы для базовых классов. Это действительно просто "лучшая практика".
Ответ 2
a) библиотека потока создается для наследования:)
Ответ 3
Что касается вашей части b, из 17.3.1.2 "Требования", пункт 1:
Библиотека может быть расширена с помощью программы на С++. Каждое предложение, если применимо, описывает требования, которые должны удовлетворять такие расширения. Такие расширения обычно являются одним из следующих:
- Аргументы шаблона
- Производные классы
- Контейнеры, итераторы и/или алгоритмы, удовлетворяющие условиям интерфейса
В то время как 17.3 является информативным, а не обязательным, намерение комитета по производному классу очевидно.
Для других очень похожих точек расширения существуют четкие требования:
- 17.1.15 "требуемое поведение" охватывает замену (оператор new и т.д.) и функции обработчика (завершающие обработчики и т.д.) и бросает все несовместимые действия в UB-land.
- 17.4.3.6/1: "В некоторых случаях (функции замены, функции обработчика, операции с типами, используемыми для создания стандартных компонентов шаблона библиотеки), стандартная библиотека С++ зависит от компонентов, поставляемых программой на С++. Если эти компоненты не отвечают их требованиям, Стандарт не предъявляет никаких требований к реализации".
В последнем пункте мне не ясно, что список в скобках является исчерпывающим, но учитывая, как конкретно каждый упомянутый случай рассматривается в следующем параграфе, было бы сказано, что текущий текст предназначен для охвата производных классов, Кроме того, этот текст 17.4.3.6/1 не изменился в проекте 2008 года (где он в 17.6.4.8), и я не вижу никаких проблем, это виртуальные методы или производные классы.
Ответ 4
Стандартная библиотека С++ - это не единое целое. Это результат объединения и принятия нескольких разных библиотек (большая часть стандартной библиотеки C, библиотека iostreams и STL являются тремя основными строительными блоками, и каждая из них была указана независимо)
Часть STL библиотеки, как вы знаете, обычно не должна быть получена. Он использует общее программирование и обычно избегает ООП.
Библиотека IOStreams является гораздо более традиционным ООП и сильно использует наследование и динамический полиморфизм, и пользователи, как ожидается, будут использовать те же механизмы для его расширения. Пользовательские потоки обычно записываются путем получения либо самого класса потока, либо класса streambuf
, который он использует внутренне. Оба они имеют виртуальные методы, которые могут быть переопределены в производных классах.
std::exception
- еще один пример.
И, как сказал Д.Шаули, я применил бы LSP к вашему второму вопросу. Всегда должно быть правовым заменить базовый класс на производный. Если я вызываю exception::what()
, он должен следовать контракту, указанному классом exception
, независимо от того, откуда пришел объект exception
, или действительно ли это производный класс, который был взвинчен. И в этом случае этот контракт является стандартным обещанием вернуть NTBS. Если вы сделали поведение производного класса по-другому, вы нарушите стандарт, потому что объект типа std::exception
больше не возвращает NTBS.
Ответ 5
Чтобы ответить на вопрос 2):
Я считаю, что да, они будут связаны описанием интерфейса стандарта ISO. Например, стандарт позволяет переопределять operator new
и operator delete
глобально. Тем не менее, стандарт гарантирует, что operator delete
является недействительным для нулевых указателей.
Не соблюдается это, безусловно, поведение undefined (по крайней мере, Скотт Майерс). Я думаю, мы можем сказать, что то же самое верно по аналогии для других областей стандартной библиотеки.
Ответ 6
Некоторые вещи из functional
, такие как greater<>
, less<>
и mem_fun_t
, получены из unary_operator<>
и binary_operator<>
. Но, IIRC, это только дает вам некоторые typedefs.
Ответ 7
Ловкое правило: "Любой класс может использоваться в качестве базового класса, а возможность его безопасного использования в отсутствие виртуальных методов, в том числе виртуального деструктора, - это полностью авторство". Добавление не-POD-члена в дочернем элементе std:: exception является той же ошибкой пользователя, что и в производном классе std::vector. Идея о том, что контейнеры не являются "предназначенными" для базовых классов, является инженерным примером того, что профессора литературы называют "Ошибочность авторского намерения".
Принцип IS-A доминирует. Не выводить D из B, если D не может заменить B во всех отношениях в открытом интерфейсе B, включая операцию удаления по указателю B. Если B имеет виртуальные методы, это ограничение менее обременительно; но если B имеет только невиртуальные методы, то все же возможно и законно специализироваться с наследованием.
С++ является многопараметрическим. Библиотека шаблонов использует наследование, даже наследование от классов без виртуальных деструкторов и, таким образом, демонстрирует на примере, что такие конструкции являются безопасными и полезными; были ли они предназначены, это психологический вопрос.
Ответ 8
Во втором вопросе я считаю, что ответ - да. В стандарте указано, что член std:: exception должен возвращать значение, отличное от NULL. Не имеет значения, есть ли значение стека, ссылки или указателя для std:: exception. Возврат того, что() связано стандартом.
Конечно, можно вернуть NULL. Но я считаю, что такой класс не соответствует стандартам.
Ответ 9
w.r.t вопрос 2), согласно стандарту С++, производный класс исключений должен также указывать спецификацию no-throw i.e. throw() вместе с возвратом ненулевого значения. Это во многих случаях означает, что производный класс исключений не должен использовать std::string, поскольку сам std::string может бросать в зависимости от реализации.
Ответ 10
Я знаю, что этот вопрос старый, но я хотел бы добавить здесь свой комментарий.
С некоторых лет я использую 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); }
...
};