是否可以从 STL 容器继承实现,而不是委托?

我有一个类,它适应 std: : Vector 来为特定于域的对象的容器建模。我想向用户公开大部分的 std: : Vector API,这样他们就可以使用熟悉的方法(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个转发函数!

所以我的问题是... 在这种情况下继承实现真的有那么糟糕吗?有什么风险?有没有一种更安全的方法可以在不需要太多键入的情况下实现它?我是使用实现继承的异端吗?:)

编辑:

如何清楚地说明用户不应该通过 std: : Vector < > 指针使用 MyContainer:

// non_api_header_file.h
namespace detail
{
typedef std::vector<MyObject> MyObjectBase;
}


// api_header_file.h
class MyContainer : public detail::MyObjectBase
{
// ...
};

助推库似乎一直在做这些事情。

编辑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.
}
33935 次浏览

The risk is deallocating through a pointer to the base class (delete, delete[], and potentially other deallocation methods). Since these classes (deque, map, string, etc.) don't have virtual dtors, it's impossible to clean them up properly with only a pointer to those classes:

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

That said, if you're willing to make sure you never accidentally do this, there's little major drawback to inheriting them—but in some cases that's a big if. Other drawbacks include clashing with implementation specifics and extensions (some of which may not use reserved identifiers) and dealing with bloated interfaces (string in particular). However, inheritance is intended in some cases, as container adapters like stack have a protected member c (the underlying container they adapt), and it's almost only accessible from a derived class instance.

Instead of either inheritance or composition, consider writing free functions which take either an iterator pair or a container reference, and operate on that. Practically all of <algorithm> is an example of this; and make_heap, pop_heap, and push_heap, in particular, are an example of using free functions instead of a domain-specific container.

So, use the container classes for your data types, and still call the free functions for your domain-specific logic. But you can still achieve some modularity using a typedef, which allows you to both simplify declaring them and provides a single point if part of them needs to change:

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

Notice the value_type and allocator can change without affecting later code using the typedef, and even the container can change from a deque to a vector.

In this case, inheriting is a bad idea: the STL containers do not have virtual destructors so you might run into memory leaks (plus, it's an indication that STL containers are not meant to be inherited in the first place).

If you just need to add some functionality, you can declare it in global methods, or a lightweight class with a container member pointer/reference. This off course doesn't allow you to hide methods: if that is really what you are after, then there's no other option then redeclaring the entire implementation.

It is easier to do:

typedef std::vector<MyObject> MyContainer;

Virtual dtors aside, the decision to inherit versus contain should be a design decision based the class you are creating. You should never inherit container functionality just because its easier than containing a container and adding a few add and remove functions that seem like simplistic wrappers unless you can definitively say that the class you are creating is a kind-of the container. For instance, a classroom class will often contain student objects, but a classroom isn't a kind of list of students for most purposes, so you shouldn't be inheriting from list.

You can combine private inheritance and the 'using' keyword to work around most of the problems mentioned above: Private inheritance is 'is-implemented-in-terms-of' and as it is private you cannot hold a pointer to the base class

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

The forwarding methods will be inlined away, anyhow. You will not get better performance this way. In fact, you will likely get worse performance.

As everyone has already stated, STL containers do not have virtual destructors so inheriting from them is unsafe at best. I've always considered generic programming with templates as a different style of OO - one without inheritance. The algorithms define the interface that they require. It is as close to Duck Typing as you can get in a static language.

Anyway, I do have something to add to the discussion. The way that I have created my own template specializations previously is to define classes like the following to use as base classes.

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

These classes expose the same interface as an STL container. I did like the effect of separating the modifying and non-modifying operations into distinct base classes. This has a really nice effect on const-correctness. The one downside is that you have to extend the interface if you want to use these with associative containers. I haven't run into the need though.

Always consider composition over inheritance.

Consider the case:

class __declspec(dllexport) Foo :
public std::multimap<std::string, std::string> {};

Then symbols of std::multimap will be exported into your dll, which may cause compilation error "std::multimap already defined".