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

Можно ли наследовать реализацию из контейнеров 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.
}
4b9b3361

Ответ 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

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