Não há problema em herdar a implementação de contêineres STL, em vez de delegar?

Eu tenho uma class que adapta std :: vector para modelar um contêiner de objects específicos do domínio. Eu quero expor a maior parte da API std :: vector para o usuário, para que ele possa usar methods familiares (tamanho, clear, at, etc …) e algoritmos padrão no container. Este parece ser um padrão recorrente para mim em meus projetos:

class MyContainer : public std::vector { 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) }; 

Estou ciente da prática de preferir a composição à inheritance ao reutilizar uma class para implementação – mas tem que haver um limite! Se eu fosse delegar tudo para std :: vector, haveria (pela minha conta) 32 funções de encaminhamento!

Então, minhas perguntas são … É realmente tão ruim herdar a implementação nesses casos? Quais são os riscos? Existe uma maneira mais segura de implementar isso sem muita digitação? Eu sou herege por usar inheritance de implementação? 🙂

Editar:

Que tal deixar claro que o usuário não deve usar o MyContainer via um ponteiro std :: vector :

 // non_api_header_file.h namespace detail { typedef std::vector MyObjectBase; } // api_header_file.h class MyContainer : public detail::MyObjectBase { // ... }; 

As bibliotecas de reforço parecem fazer isso o tempo todo.

Editar 2:

Uma das sugestões foi usar funções livres. Mostrarei aqui como pseudo-código:

 typedef std::vector MyCollection; void specialCollectionInitializer(MyCollection& c, arguments...); result specialCollectionFunction(const MyCollection& c); etc... 

Uma maneira mais OO de fazer isso:

 typedef std::vector 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. } 

O risco é desalocar por meio de um ponteiro para a class base ( delete , delete [] e potencialmente outros methods de desalocação). Como essas classs ( deque , map , string , etc.) não possuem dtors virtuais, é impossível limpá-las corretamente apenas com um ponteiro para essas classs:

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

Dito isso, se você estiver disposto a garantir que nunca faça isso acidentalmente, há uma grande desvantagem em herdá-los – mas, em alguns casos, é um grande problema. Outras desvantagens incluem conflito com especificidades de implementação e extensões (algumas das quais podem não usar identificadores reservados) e lidar com interfaces inchadas ( string em particular). No entanto, a inheritance é planejada em alguns casos, já que os adaptadores de contêiner como stack possuem um membro protegido c (o contêiner subjacente que eles adaptam) e é quase acessível somente a partir de uma instância de class derivada.

Em vez de inheritance ou composição, considere escrever funções livres que utilizem um par iterador ou uma referência de contêiner e opere sobre ele. Praticamente todos os são um exemplo disso; e make_heap , pop_heap e push_heap , em particular, são um exemplo do uso de funções livres em vez de um contêiner específico do domínio.

Portanto, use as classs de contêiner para seus tipos de dados e ainda chame as funções gratuitas para sua lógica específica de domínio. Mas você ainda pode conseguir alguma modularidade usando um typedef, o que permite que você simplifique declará-los e forneça um único ponto, se parte deles precisar mudar:

 typedef std::deque Example; // ... Example c (42); example_algorithm(c); example_algorithm2(c.begin() + 5, c.end() - 5); Example::iterator i; // nested types are especially easier 

Observe que o value_type e o allocator podem ser alterados sem afetar o código posterior usando o typedef e até mesmo o contêiner pode mudar de um deque para um vetor .

Você pode combinar inheritance privada e a palavra-chave ‘using’ para contornar a maioria dos problemas mencionados acima: A inheritance privada é ‘é-implementada-em-termos-de’ e como é privada, você não pode manter um ponteiro para a class base

 #include  #include  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; } 

Como todos já afirmaram, os contêineres STL não possuem destruidores virtuais, portanto, herdar deles não é seguro, na melhor das hipóteses. Eu sempre considerei programação genérica com templates como um estilo diferente de OO – um sem inheritance. Os algoritmos definem a interface que eles exigem. Está tão perto de Duck Typing quanto você pode entrar em uma linguagem estática.

De qualquer forma, tenho algo a acrescentar à discussão. A maneira que eu criei minhas próprias especializações de modelo anteriormente é definir classs como as seguintes para usar como classs base.

 template  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  class writable_container_facade: public readable_container_facade { 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::operator=(other); return *this; } }; 

Essas classs expõem a mesma interface que um contêiner STL. Eu gostei do efeito de separar as operações modificadoras e não modificadoras em classs base distintas. Isso tem um efeito muito bom sobre a correção de const. A única desvantagem é que você tem que estender a interface, se você quiser usá-los com contêineres associativos. Eu não corri para a necessidade embora.

Nesse caso, herdar é uma má idéia: os contêineres STL não têm destruidores virtuais, portanto, você pode ter vazamentos de memory (além disso, é uma indicação de que os contêineres STL não devem ser herdados em primeiro lugar).

Se você só precisar adicionar alguma funcionalidade, poderá declará-la em methods globais ou em uma class leve com um ponteiro / referência de membro de contêiner. Este curso não permite que você esconda methods: se isso é realmente o que você procura, então não há outra opção e redeclarando toda a implementação.

Virtual dtors à parte, a decisão de herdar versus conter deve ser uma decisão de design baseada na class que você está criando. Você nunca deve herdar a funcionalidade de contêiner apenas porque é mais fácil do que conter um contêiner e adicionar algumas funções de adicionar e remover que parecem invólucros simplistas, a menos que você possa dizer definitivamente que a class que está criando é do tipo contêiner. Por exemplo, uma turma de sala de aula geralmente contém objects de alunos, mas uma sala de aula não é um tipo de lista de alunos para a maioria das finalidades, então você não deveria estar herdando da lista.

É mais fácil de fazer:

 typedef std::vector MyContainer; 

Os methods de encaminhamento serão eliminados de qualquer maneira. Você não obterá melhor desempenho dessa maneira. Na verdade, você provavelmente terá um desempenho pior.