Можно ли наследовать реализацию от контейнеров STL, а не делегировать? - PullRequest
71 голосов
/ 09 января 2010

У меня есть класс, который адаптирует 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 функции пересылки!

Итак, мои вопросы ... Неужели так плохо наследовать реализацию в таких случаях? Каковы риски? Есть ли более безопасный способ, которым я могу реализовать это без так много печатать? Я еретик для использования наследования реализации? :)

* * 1008 Edit: * * 1010

Как насчет того, чтобы дать понять, что пользователь не должен использовать 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, кажется, делают это постоянно.

Редактировать 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.
}

Ответы [ 7 ]

69 голосов
/ 10 января 2010

Риск освобождается через указатель на базовый класс ( delete , delete [] и, возможно, другие методы освобождения). Поскольку эти классы ( deque , map , string и т. Д.) Не имеют виртуальных dtors, их невозможно очистить должным образом, используя только указатель на эти классы:

struct BadExample : vector<int> {};
int main() {
  vector<int>* p = new BadExample();
  delete p; // this is Undefined Behavior
  return 0;
}

Тем не менее, , если вы готовы убедиться, что никогда не сделаете это случайно, у них мало серьезных недостатков в их наследовании - но в некоторых случаях это большое условие. Другие недостатки включают конфликты со спецификой реализации и расширениями (некоторые из которых могут не использовать зарезервированные идентификаторы) и работу с раздутыми интерфейсами (в частности string ). Однако в некоторых случаях наследование предназначено, поскольку адаптеры контейнеров, такие как stack , имеют защищенный член 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 и allocator могут меняться, не влияя на более поздний код с использованием typedef, и даже контейнер может меняться с deque на vector .

30 голосов
/ 10 января 2010

Вы можете комбинировать частное наследование и ключевое слово «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;
}
13 голосов
/ 10 января 2010

Как все уже заявили, контейнеры STL не имеют виртуальных деструкторов, поэтому наследование от них в лучшем случае небезопасно. Я всегда рассматривал общее программирование с шаблонами как другой стиль ОО - один без наследования. Алгоритмы определяют интерфейс, который им требуется. Это так близко к 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. Мне очень понравился эффект разделения модифицирующих и немодифицирующих операций на отдельные базовые классы. Это очень хорошо влияет на правильность. Единственным недостатком является то, что вам нужно расширить интерфейс, если вы хотите использовать их с ассоциативными контейнерами. Я не столкнулся с необходимостью, хотя.

5 голосов
/ 10 января 2010

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

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

2 голосов
/ 10 января 2010

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

1 голос
/ 10 января 2010

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

1 голос
/ 10 января 2010

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

typedef std::vector<MyObject> MyContainer;
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...