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


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

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

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

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

10 52

10 ответов:

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

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

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

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

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

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

Что касается вашей части в, то из пункта 1 пункта 17.3.1.2 "требования":

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

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

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

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

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

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

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

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

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

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

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

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

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

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

некоторые вещи в functional, Как greater<>,less<> и mem_fun_t полученные от unary_operator<> и binary_operator<>. Но, IIRC, это дает вам только некоторые typedefs.

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

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

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

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

конечно, это возможно, чтобы возвращать null. Но я бы счел такой класс несоответствующим стандартам.

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

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

уже несколько лет я использую 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); }
    ...
};