Можно ли наследовать реализацию от контейнеров STL, а не делегировать?
у меня есть класс, который адаптирует std::vector для моделирования контейнера доменных объектов. Я хочу предоставить большую часть API std::vector пользователю, чтобы он мог использовать знакомые методы (size, clear, at и т. д...) и стандартные алгоритмы на контейнере. Это, кажется, повторяющийся шаблон для меня в моих проектах:
class MyContainer : public std::vector<MyObject>
{
public:
// Redeclare all container traits: value_type, iterator, etc...
// Domain-specific constructors
// (more useful to the user than std::vector ones...)
// Add a few domain-specific helper methods...
// Perhaps modify or hide a few methods (domain-related)
};
Я знаю о практике предпочтения композиции наследованию при повторном использовании класса для реализации - но должен быть предел! если я должны были делегировать все в std:: vector, было бы (по моему подсчету) 32 функции пересылки!
Итак, мои вопросы... Действительно ли так плохо наследовать реализацию в таких случаях? Каковы риски? Есть ли более безопасный способ реализовать это без такого большого набора текста? Я еретик для использования наследования реализации? :)
Edit:
как насчет того, чтобы дать понять, что пользователь не должен использовать MyContainer через std:: vector указатель:
// non_api_header_file.h
namespace detail
{
typedef std::vector<MyObject> MyObjectBase;
}
// api_header_file.h
class MyContainer : public detail::MyObjectBase
{
// ...
};
библиотеки boost, похоже, делают это все время.
Edit 2:
одним из предложений было использовать бесплатные функции. Я покажу его здесь как псевдо-код:
typedef std::vector<MyObject> MyCollection;
void specialCollectionInitializer(MyCollection& c, arguments...);
result specialCollectionFunction(const MyCollection& c);
etc...
более OO способ сделать это:
typedef std::vector<MyObject> MyCollection;
class MyCollectionWrapper
{
public:
// Constructor
MyCollectionWrapper(arguments...) {construct coll_}
// Access collection directly
MyCollection& collection() {return coll_;}
const MyCollection& collection() const {return coll_;}
// Special domain-related methods
result mySpecialMethod(arguments...);
private:
MyCollection coll_;
// Other domain-specific member variables used
// in conjunction with the collection.
}
7 ответов:
риск удаления через указатель на базовый класс (удалить,удалить[], и потенциально другие методы освобождения). Так как эти классы (deque,карта,строка и т. д.) у вас нет виртуальных dtors, их невозможно правильно очистить только указателем на эти классы:
struct BadExample : vector<int> {}; int main() { vector<int>* p = new BadExample(); delete p; // this is Undefined Behavior return 0; }
что сказал:если вы готовы убедиться, что вы никогда случайно не делаете это, есть небольшой серьезный недостаток в наследовании их-но в некоторых случаях это большое если. Другие недостатки включают столкновение с особенностями реализации и расширениями (некоторые из которых могут не использовать зарезервированные идентификаторы) и работу с раздутыми интерфейсами (строка в частности). Однако наследование предназначено в некоторых случаях, как контейнерные адаптеры, такие как стек есть защищенный член c (базовый контейнер они адаптируют), и это почти только доступно из экземпляра производного класса.
вместо наследования или композиции, рассмотрите возможность написания свободных функций которые берут либо пару итераторов, либо ссылку на контейнер и работают над этим. Практически весь является примером этого; и make_heap,pop_heap и push_heap, в частности, являются примером использования свободных функций вместо контейнера для конкретного домена.
Итак, использовать контейнерные классы для ваших типов данных и все еще вызывают свободные функции для вашей доменной логики. Но вы все равно можете достичь некоторой модульности с помощью typedef, что позволяет упростить их объявление и обеспечивает одну точку, если часть из них нужно изменить:
typedef std::deque<int, MyAllocator> Example; // ... Example c (42); example_algorithm(c); example_algorithm2(c.begin() + 5, c.end() - 5); Example::iterator i; // nested types are especially easier
обратите внимание, что value_type и распределитель могут измениться, не влияя на более поздний код с помощью typedef, и даже контейнер может измениться с deque в вектор.
вы можете объединить частное наследование и ключевое слово "using", чтобы обойти большинство проблем, упомянутых выше: частное наследование "реализовано в терминах", и поскольку оно является частным, вы не можете удерживать указатель на базовый класс
#include <string> #include <iostream> class MyString : private std::string { public: MyString(std::string s) : std::string(s) {} using std::string::size; std::string fooMe(){ return std::string("Foo: ") + *this; } }; int main() { MyString s("Hi"); std::cout << "MyString.size(): " << s.size() << std::endl; std::cout << "MyString.fooMe(): " << s.fooMe() << std::endl; }
как все уже заявили, контейнеры STL не имеют виртуальных деструкторов, поэтому наследование от них в лучшем случае небезопасно. Я всегда рассматривал общее Программирование с шаблонами как другой стиль OO - one без наследования. Алгоритмы определяют интерфейс, который им требуется. Это как можно ближе к Утиной Типизацией как вы можете получить в статический язык.
в любом случае, у меня есть что добавить к обсуждению. То, как я создал свой собственный шаблон специализации ранее заключается в определении классов, как показано ниже, чтобы использовать в качестве базовых классов.
template <typename Container> class readonly_container_facade { public: typedef typename Container::size_type size_type; typedef typename Container::const_iterator const_iterator; virtual ~readonly_container_facade() {} inline bool empty() const { return container.empty(); } inline const_iterator begin() const { return container.begin(); } inline const_iterator end() const { return container.end(); } inline size_type size() const { return container.size(); } protected: // hide to force inherited usage only readonly_container_facade() {} protected: // hide assignment by default readonly_container_facade(readonly_container_facade const& other): : container(other.container) {} readonly_container_facade& operator=(readonly_container_facade& other) { container = other.container; return *this; } protected: Container container; }; template <typename Container> class writable_container_facade: public readable_container_facade<Container> { public: typedef typename Container::iterator iterator; writable_container_facade(writable_container_facade& other) readonly_container_facade(other) {} virtual ~writable_container_facade() {} inline iterator begin() { return container.begin(); } inline iterator end() { return container.end(); } writable_container_facade& operator=(writable_container_facade& other) { readable_container_facade<Container>::operator=(other); return *this; } };
эти классы предоставляют тот же интерфейс, что и контейнер STL. Мне нравится эффект разделения модифицируемые и немодифицируемые операций в различных базовых классов. Это очень хорошо влияет на const-корректность. Один недостаток заключается в том, что вам нужно расширить интерфейс, если вы хотите использовать их с ассоциативными контейнерами. Я не столкнулся с необходимостью хотя.
в этом случае наследование-плохая идея: контейнеры STL не имеют виртуальных деструкторов, поэтому вы можете столкнуться с утечками памяти (кроме того, это указывает на то, что контейнеры STL не предназначены для наследования в первую очередь).
Если вам просто нужно добавить некоторую функциональность, вы можете объявить ее в глобальных методах или в облегченном классе с указателем/ссылкой на член контейнера. Это, конечно, не позволяет вам скрывать методы: если это действительно то, что вам нужно, то нет другого варианта, чем повторное объявление всей реализации.
виртуальные dtors в сторону, решение наследовать по сравнению с contain должно быть проектным решением на основе класса, который вы создаете. Вы никогда не должны наследовать функциональность контейнера только потому, что это проще, чем содержать контейнер и добавлять несколько функций добавления и удаления, которые кажутся упрощенными оболочками если вы можете окончательно сказать, что класс, который вы создаете, является своего рода контейнером. Например, класс classroom часто содержит объекты student, но a класс не является своего рода списком студентов для большинства целей, поэтому вы не должны наследовать от списка.