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

Emi*_*ier 73 c++ stl

我有一个类,它适应std :: vector来模拟特定于域的对象的容器.我想向用户公开大多数std :: vector API,以便他/她可以在容器上使用熟悉的方法(大小,清晰,等等......)和标准算法.在我的设计中,这似乎是一种反复出现的模式:

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)
};
Run Code Online (Sandbox Code Playgroud)

我知道在重用类实现时更喜欢使用组合继承的做法 - 但是必须有限制!如果我将所有内容委托给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
{
   // ...
};
Run Code Online (Sandbox Code Playgroud)

boost库似乎总是这样做.

编辑2:

其中一个建议是使用免费功能.我会在这里将其显示为伪代码:

typedef std::vector<MyObject> MyCollection;
void specialCollectionInitializer(MyCollection& c, arguments...);
result specialCollectionFunction(const MyCollection& c);
etc...
Run Code Online (Sandbox Code Playgroud)

更多的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.
}
Run Code Online (Sandbox Code Playgroud)

小智 73

风险是通过指向基类的指针(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;
}
Run Code Online (Sandbox Code Playgroud)

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

而不是继承或组合,考虑编写带有迭代器对或容器引用的自由函数,并对其进行操作.几乎所有<algorithm>都是这样的一个例子; 和make_heap,pop_heappush_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
Run Code Online (Sandbox Code Playgroud)

请注意,value_type和allocator可以在不影响使用typedef的后续代码的情况下进行更改,甚至容器也可以从deque更改为vector.


Ben*_*Ben 32

您可以结合使用私有继承和'using'关键字来解决上面提到的大多数问题:私有继承是'is-implemented-in-terms-of',因为它是私有的,你不能保存指向基类的指针

#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;
}
Run Code Online (Sandbox Code Playgroud)

  • 私有继承和私有数据成员在更改时都会破坏二进制兼容性,除了朋友(应该很少)之外,通常不难在它们之间切换 - 使用它通常由实现细节决定.另请参阅"base-from-member idiom". (7认同)
  • 我不禁要提到"私有"继承仍然是继承,因此比组合更强大的关系.值得注意的是,这意味着更改类的实现必然会破坏二进制兼容性. (2认同)

D.S*_*ley 14

正如大家已经说过的那样,STL容器没有虚拟析构函数,因此从它们继承是最不安全的.我一直认为使用模板的泛型编程是一种不同的OO风格 - 一种没有继承.算法定义了它们所需的接口.它与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;
    }
};
Run Code Online (Sandbox Code Playgroud)

这些类公开与STL容器相同的接口.我确实喜欢将修改和非修改操作分成不同的基类的效果.这对const正确性有很好的影响.一个缺点是,如果要将这些与关联容器一起使用,则必须扩展接口.我没有遇到过需要.


Jhe*_*ico 6

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


sti*_*ijn 5

在这种情况下,继承是一个坏主意:STL容器没有虚拟析构函数,因此您可能会遇到内存泄漏(此外,它表明STL容器首先不应该继承).

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