Подтвердить что ты не робот

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

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

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

4b9b3361

Ответ 1

Есть несколько причин, почему это плохая идея.

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

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

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

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

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

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

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

Ответ 2

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

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

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

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

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

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

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

Ответ 3

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

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

Ответ 4

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

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

Ответ 5

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

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

#include <vector>
#include <iostream>

// private inheritance, nobody else knows about the inheritance, so nobody is upcasting my
// container to a std::vector
template <class T> class MyVector : private std::vector<T>
{
private:
    // in case I changed to boost or something later, I don't have to update everything below
    typedef std::vector<T> 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<int> 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<int>* stdVector = &intArray;

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

error C2243: 'type cast': conversion from 'MyVector<int> *' to 'std::vector<T,std::allocator<_Ty>> *' exists, but is inaccessible

Ответ 6

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

Ответ 7

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

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

Ответ 8

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

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

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

GizmoList aList = GetGizmos();

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

Ответ 9

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

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

template<typename T>
struct MyVector : std::vector<T> {};

std::vector<int>* p = new MyVector<int>;
//....
delete p; // oops "Undefined Behavior"; as vector::~vector() is not 'virtual'

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

Если я хочу принять экстренную помощь, то я могу перейти к этому:

#include<vector>
template<typename T>
struct MyVector : std::vector<T> {};
#define vector DONT_USE

Что будет запрещено с помощью vector целиком.