Можно ли наследовать реализацию из контейнеров STL, а не делегировать?
У меня есть класс, который адаптирует std::vector для моделирования контейнера объектов, специфичных для домена. Я хочу выставить большую часть API std::vector пользователю, чтобы он/она мог использовать знакомые методы (размер, прозрачность, 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 < > pointer:
// non_api_header_file.h
namespace detail
{
typedef std::vector<MyObject> MyObjectBase;
}
// api_header_file.h
class MyContainer : public detail::MyObjectBase
{
// ...
};
Библиотеки boost, похоже, все время делают это.
Изменить 2:
Одним из предложений было использование бесплатных функций. Я покажу его здесь как псевдокод:
typedef std::vector<MyObject> MyCollection;
void specialCollectionInitializer(MyCollection& c, arguments...);
result specialCollectionFunction(const MyCollection& c);
etc...
Еще один способ сделать это:
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.
}
Ответы
Ответ 1
Риск освобождается от указателя на базовый класс (удалить, удалить [] и, возможно, другие методы удаления]. Поскольку эти классы (deque, map, string и т.д.) Не имеют виртуальных 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, и даже контейнер может перейти от дека к вектору.
Ответ 2
Вы можете комбинировать личное наследование и ключевое слово "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;
}
Ответ 3
Как уже было сказано, контейнеры STL не имеют виртуальных деструкторов, поэтому наследование от них в лучшем случае небезопасно. Я всегда рассматривал общее программирование с шаблонами как другой стиль OO - один без наследования. Алгоритмы определяют требуемый интерфейс. Это как можно ближе к Duck Typing, поскольку вы можете получить статический язык.
Во всяком случае, мне есть что добавить в дискуссию. Раньше я создал свои собственные специализированные шаблоны, чтобы определить классы, подобные следующим, для использования в качестве базовых классов.
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-correctness. Единственным недостатком является то, что вы должны расширить интерфейс, если хотите использовать его с ассоциативными контейнерами. Однако я не столкнулся с необходимостью.
Ответ 4
В этом случае наследование - это плохая идея: контейнеры STL не имеют виртуальных деструкторов, поэтому вы можете столкнуться с утечками памяти (плюс, это указание на то, что контейнеры STL не должны наследоваться в первую очередь).
Если вам просто нужно добавить некоторые функции, вы можете объявить его в глобальных методах или в легком классе с указателем/ссылкой на контейнер. Этот курс не позволяет вам скрывать методы: если это действительно то, что вам нужно, тогда нет другого варианта, кроме повторной реализации всей реализации.
Ответ 5
Виртуальные dtors в сторону, решение наследовать против содержания должно быть дизайнерским решением, основанным на классе, который вы создаете. Вы никогда не должны наследовать функциональность контейнера только потому, что его проще, чем содержать контейнер, и добавлять несколько функций добавления и удаления, которые выглядят как упрощенные обертки, если вы не можете окончательно сказать, что класс, который вы создаете, является своего рода контейнером. Например, класс класса часто содержит объекты-ученики, но класс не является списком учеников для большинства целей, поэтому вы не должны наследовать из списка.
Ответ 6
Это проще сделать:
typedef std::vector<MyObject> MyContainer;
Ответ 7
В любом случае методы пересылки будут отклонены. Таким образом, вы не получите лучшей производительности. Фактически, вы, вероятно, получите худшую производительность.