关于c ++:从STL容器继承实现是否可以,而不是委托?

Is it okay to inherit implementation from STL containers, rather than delegate?

我有一个类,它使std::vector适应于为特定于域的对象的容器建模。我想向用户公开std::vector API的大部分内容,这样他/她就可以在容器上使用熟悉的方法(size、clear、at等)和标准算法。在我的设计中,这似乎是一个反复出现的模式:

1
2
3
4
5
6
7
8
9
10
11
12
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,该怎么办?

1
2
3
4
5
6
7
8
9
10
11
// non_api_header_file.h
namespace detail
{
   typedef std::vector<MyObject> MyObjectBase;
}

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

Boost图书馆似乎一直在做这些事情。

编辑2:

其中一个建议是使用自由函数。我将在这里显示为伪代码:

1
2
3
4
typedef std::vector<MyObject> MyCollection;
void specialCollectionInitializer(MyCollection& c, arguments...);
result specialCollectionFunction(const MyCollection& c);
etc...

更糟糕的方法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.
}


风险是通过指向基类(delete、delete[]和可能的其他释放方法)的指针释放的。由于这些类(deque、map、string等)没有虚拟DTR,因此仅使用指向这些类的指针就无法正确地清除它们:

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

也就是说,如果您愿意确保自己不会意外地做到这一点,那么继承它们就没有什么大缺点,但在某些情况下,这是一个很大的if。其他缺点包括与实现细节和扩展冲突(其中一些可能不使用保留标识符),以及处理膨胀的接口(特别是字符串)。但是,继承在某些情况下是有意的,因为像stack这样的容器适配器有一个受保护的成员C(它们所适应的底层容器),并且它几乎只能从派生类实例访问。

代替继承或组合,考虑编写自由函数,这些函数接受迭代器对或容器引用,并对其进行操作。实际上,所有这些都是一个例子;特别是make-heap、pop-heap和push-heap是一个使用自由函数而不是特定于域的容器的例子。

因此,对数据类型使用容器类,并仍然为特定于域的逻辑调用自由函数。但是,您仍然可以使用typedef实现一些模块化,这样既可以简化声明,也可以在其中一部分需要更改时提供一个单点:

1
2
3
4
5
6
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

注意,值"type"和"allocator"可以更改,而不会影响以后使用typedef的代码,甚至容器也可以从deque更改为vector。


您可以将私有继承和"using"关键字结合起来解决上面提到的大多数问题:私有继承是"根据"实现的,并且由于它是私有的,所以不能持有指向基类的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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风格。这些算法定义了它们需要的接口。在静态语言中,它尽可能接近duck类型。

不管怎样,我确实有一些东西要添加到讨论中。我以前创建自己模板专门化的方法是定义如下类作为基类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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容器相同的接口。我确实喜欢将修改操作和非修改操作分离为不同的基类的效果。这对常量正确性有很好的影响。一个缺点是,如果要将这些与关联容器一起使用,则必须扩展接口。不过,我还没有遇到这种需要。


在这种情况下,继承是一个坏主意:STL容器没有虚拟析构函数,因此您可能会遇到内存泄漏(另外,这表明STL容器一开始并不打算继承)。

如果您只需要添加一些功能,那么可以在全局方法中声明它,或者使用容器成员指针/引用的轻量级类。这一偏离的过程不允许您隐藏方法:如果这确实是您所追求的,那么就没有其他选择了,然后重新声明整个实现。


除了虚拟数据,继承和包含的决策应该是基于您正在创建的类的设计决策。您永远不应该继承容器功能,因为它比包含一个容器和添加一些看起来像简单包装器的添加和删除函数更容易,除非您可以明确地说您正在创建的类是一种容器。例如,教室类通常包含学生对象,但在大多数情况下,教室不是学生列表,因此您不应该从列表继承。


无论如何,转发方法将被内联。这样你不会得到更好的表现。事实上,你的表现可能会更差。


更容易做到:

1
typedef std::vector<MyObject> MyContainer;