Подclass / наследование стандартных контейнеров?

Я часто читаю эти утверждения в Stack Overflow. Лично я не нахожу никаких проблем с этим, если я не использую его полиморфным образом; т.е. где я должен использовать virtual деструктор.

Если я хочу расширить / добавить функциональность стандартного контейнера, то что лучше, чем наследование? Упаковка этого контейнера внутри пользовательского classа требует гораздо больших усилий и по-прежнему нечиста.

Существует ряд причин, почему это плохая идея.

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

Основные правила для виртуальных dtors

Во-вторых, это действительно плохой дизайн. И есть несколько причин, по которым это плохой дизайн. Во-первых, вы всегда должны расширять функциональность стандартных контейнеров с помощью алгоритмов, которые работают в целом. Это простая причина сложности – если вам нужно написать алгоритм для каждого контейнера, к которому он применяется, и у вас есть M-контейнеры и N алгоритмов, то есть методы M x N, которые вы должны написать. Если вы пишете свои алгоритмы в целом, у вас есть только N алгоритмов. Поэтому вы получаете гораздо больше повторного использования.

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

Классическое описание инкапсуляции

Наконец, в общем, вы никогда не должны думать о наследовании как о средстве расширения поведения classа. Это одна из большой, плохой лжи ранней теории ООП, которая возникла из-за неясного мышления о повторном использовании, и ее продолжают преподавать и продвигать по сей день, хотя существует четкая теория, почему это плохо. Когда вы используете наследование для расширения поведения, вы связываете это расширенное поведение с контрактом интерфейса таким образом, чтобы привязывать пользователей к будущим изменениям. Например, предположим, что у вас есть class типа Socket, который обменивается данными с использованием протокола TCP, и вы расширяете его поведение, вызывая class SSLSocket от Socket и реализуя поведение более высокого протокола стека SSL поверх Socket. Теперь предположим, что вы получаете новое требование иметь один и тот же протокол связи, но через линию USB или по телефону. Вам нужно будет вырезать и вставить всю эту работу в новый class, который происходит из classа USB или classа Telephony. И теперь, если вы обнаружите ошибку, вы должны ее исправить во всех трех местах, что не всегда произойдет, а это значит, что ошибки будут занимать больше времени и не всегда исправляться …

Это общее для любой иерархии наследования A-> B-> C -> … Если вы хотите использовать поведение, которое вы расширили в производных classах, таких как B, C, .. на объектах не базового classа A, вы должны перепроектировать или дублировать реализацию. Это приводит к очень monoлитным проектам, которые очень трудно изменить по дороге (подумайте, что Microsoft MFC или их .NET, или – ну, они делают эту ошибку много). Вместо этого вы всегда должны думать о расширении через композицию, когда это возможно. Наследование должно использоваться, когда вы думаете «Открытый / Закрытый Принцип». Вы должны обладать абстрактными базовыми classами и динамическим polymorphismом через унаследованный class, каждый из которых будет полностью реализован. Иерархии не должны быть глубокими – почти всегда два уровня. Используйте только более двух, если у вас есть разные динамические категории, которые относятся к различным функциям, которые нуждаются в этом различии для безопасности типов. В этих случаях используйте абстрактные базы до classов листьев, которые имеют реализацию.

Может быть, многим людям здесь не понравится этот ответ, но пришло время рассказать о какой-то ереси и да … также сказать, что «король голый!»

Вся мотивация к деривации слаба. Вывод не отличается от композиции. Это всего лишь способ «соединить вещи». Композиция объединяет вещи, давая им имена, наследование делает это без указания явных имен.

Если вам нужен вектор, который имеет тот же интерфейс и реализацию std :: vect плюс что-то еще, вы можете:

использовать композицию и переписать все прототипы встроенных объектов, реализующих функцию, которая делегирует их (и если они 10000 … да: будьте готовы переписать все эти 10000) или …

наследовать его и добавлять только то, что вам нужно (и … просто переписать конструкторы, пока юристы C ++ не решат позволить им наследоваться также: я до сих пор помню 10-летнюю фанатичную дискуссию о том, «почему которы не могут называть друг друга» и почему это это «плохая дурная вещь» … пока C ++ 11 не разрешил это, и внезапно все эти фанатики замолчали!) и пусть новый деструктор не виртуальный, как оригинальный.

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

Все «избегайте этого» «не делайте этого» всегда звучат как «морализация» чего-то, что является изначально агностиком. Все функции языка существуют для решения какой-либо проблемы. Тот факт, что данный способ решения проблемы является хорошим или плохим, зависит от контекста, а не от самой функции. Если то, что вы делаете, должно обслуживать множество контейнеров, наследование, вероятно, не так (вы должны повторить для всех). Если это для конкретного случая … наследование – способ создания. Забудьте о пуризмах ООП: C ++ – это не «чистый ООП», а контейнер вообще не ООП.

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

Кстати, если вы хотите добавить кучу алгоритмов в контейнер, подумайте о том, чтобы добавить их в качестве автономных функций с диапазоном iteratorов.

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

Необходимость написать некоторые функции пересылки выглядит как небольшая цена для сравнения.

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

С другой стороны, частное наследство – это не проблема. Рассмотрим следующий пример:

 #include  #include  // private inheritance, nobody else knows about the inheritance, so nobody is upcasting my // container to a std::vector template  class MyVector : private std::vector { private: // in case I changed to boost or something later, I don't have to update everything below typedef std::vector base_vector; public: typedef typename base_vector::size_type size_type; typedef typename base_vector::iterator iterator; typedef typename base_vector::const_iterator const_iterator; using base_vector::operator[]; using base_vector::begin; using base_vector::clear; using base_vector::end; using base_vector::erase; using base_vector::push_back; using base_vector::reserve; using base_vector::resize; using base_vector::size; // custom extension void reverse() { std::reverse(this->begin(), this->end()); } void print_to_console() { for (auto it = this->begin(); it != this->end(); ++it) { std::cout << *it << '\n'; } } }; int main(int argc, char** argv) { MyVector intArray; intArray.resize(10); for (int i = 0; i < 10; ++i) { intArray[i] = i + 1; } intArray.print_to_console(); intArray.reverse(); intArray.print_to_console(); for (auto it = intArray.begin(); it != intArray.end();) { it = intArray.erase(it); } intArray.print_to_console(); return 0; } 

ВЫВОД:

 1 2 3 4 5 6 7 8 9 10 10 9 8 7 6 5 4 3 2 1 

Чисто и просто, и дает вам свободу в расширении std контейнеров без особых усилий.

И если вы думаете о том, чтобы делать что-то глупое, вот так:

 std::vector* stdVector = &intArray; 

Вы получите следующее:

 error C2243: 'type cast': conversion from 'MyVector *' to 'std::vector> *' exists, but is inaccessible 

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

Наиболее распространенная причина, по которой нужно наследовать от контейнеров, заключается в том, что вы хотите добавить некоторую функцию-член в class. Поскольку сам stdlib не модифицируется, считается, что наследование является заменой. Однако это не работает. Лучше сделать свободную функцию, которая принимает вектор как параметр:

 void f(std::vector &v) { ... } 

Я иногда наследую от типов коллекций просто как лучший способ назвать типы.
Мне не нравится typedef как личное предпочтение. Поэтому я сделаю что-то вроде:

 class GizmoList : public std::vector { /* No Body & no changes. Just a more descriptive name */ }; 

Тогда гораздо проще и понятнее написать:

 GizmoList aList = GetGizmos(); 

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

IMHO, я не нахожу никакого вреда в наследовании контейнеров STL, если они используются в качестве расширений функциональности . (Вот почему я задал этот вопрос 🙂 🙂

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

 template struct MyVector : std::vector {}; std::vector* p = new MyVector; //.... delete p; // oops "Undefined Behavior"; as vector::~vector() is not 'virtual' 

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

Если я хочу проявлять особую осторожность, я могу пойти на это:

 #include template struct MyVector : std::vector {}; #define vector DONT_USE 

Который полностью запретит использование vector .

Давайте будем гением компьютера.