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

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 класс не является своего рода списком студентов для большинства целей, поэтому вы не должны наследовать от списка.

это проще сделать:

typedef std::vector<MyObject> MyContainer;

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