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


78

我有一个适用于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)
};

我知道在将类重用于实现时偏向于继承而不是继承的做法-但一定有限制!如果我将所有内容都委派给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
{
   // ...
};

Boost库似乎一直在执行此操作。

编辑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.
}

6
哦,天哪!另一个机会在punch.wordpress.com上推送我的博客-基本上,编写免费功能,而忘记“更多面向对象”包装方法。它不是OO-如果是的话,它将使用继承,在这种情况下,您可能不应该使用继承。记住OO!= class。

1
@Neil:但是,但是..全局函数是邪恶的!!!一切都是对象!;)
Emile Cormier 2010年

4
如果将它们放在命名空间中,它们将不是全局的。

1
如果您确实想公开vector的整个接口,那么在C ++中使用composition并通过getter(带有const和非const版本)公开对vector的引用可能更好。在Java中,您只是继承而已,但是在Java中,有些笨拙将不会出现,忽略您的文档,通过错误的指针删除对象(或再次继承并将其弄乱),然后抱怨。也许对于有限的受众来说,但是如果用户可能是动态多态狂或最近的Java程序员,那么您正在设计一个可以确定他们会误解的接口。
史蒂夫·杰索普

1
您无法防止人们完全忽略文档。我发现Java中的此类问题与C ++中的许多问题一样令人惊讶。

Answers:


75

风险是通过指向基类的指针deletedelete []和可能的其他释放方法)进行分配。由于这些类(dequemapstring等)没有虚拟dtor,因此仅使用指向这些类的指针就无法正确清理它们:

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

就是说,如果您愿意确保自己绝不会意外执行此操作,则继承它们几乎没有什么重大缺点-但是在某些情况下,如果这样做的话会很大。其他缺点包括与实现细节和扩展冲突(其中一些可能不使用保留的标识符)以及处理过大的接口(特别是字符串)。但是,在某些情况下是要使用继承的,因为像stack这样的容器适配器具有受保护的成员c(它们适应的基础容器),并且几乎只能从派生类实例进行访问。

考虑继承或组合,而不是考虑编写使用迭代器对或容器引用并对其进行操作的自由函数。实际上,所有<algorithm>都是一个例子。和make_heappop_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

请注意,可以使用typedef更改value_type和allocator而不影响以后的代码,甚至容器也可以从双端队列更改为vector


35

您可以结合使用私有继承和“ 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;
}

2
我不禁要提到,private继承仍然是继承,因此关系比组合更牢固。值得注意的是,这意味着更改类的实现必然会破坏二进制兼容性。
Matthieu M. 2010年

8
私有继承和私有数据成员在更改时都会破坏二进制兼容性,并且,除了朋友(应该很少)之外,通常不难在它们之间进行切换-经常由实现细节决定使用。另请参阅“成员基础成语”。

对于好奇的“成语基础”成语:en.wikibooks.org/wiki/More_C%2B%2B_Idioms/“
Emile Cormier

1
@MatthieuM。对于大多数应用程序,中断ABI根本不是问题。甚至有些库都没有Pimpl才能生存,以获得更好的性能。
doc 2014年

15

正如每个人都已经说过的那样,STL容器没有虚拟析构函数,因此从它们那里继承最多是不安全的。我一直认为带有模板的通用编程是一种不同的OO风格-一种没有继承的风格。这些算法定义了它们所需的接口。它以静态语言尽可能接近鸭子打字

无论如何,我确实有一些要补充的内容。我以前创建自己的模板专业化方法的方式是定义如下所示的类用作基类。

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容器相同的接口。我确实喜欢将修改操作和非修改操作分成不同的基类的效果。这对const正确性有非常好的影响。缺点是,如果要将它们与关联容器一起使用,则必须扩展接口。我没有遇到需要。


真好!我可能只是用那个。但是其他人对适应容器的想法进行了重新思考,所以也许我不会使用它。:)
Emile Cormier 2010年

话虽这么说,繁琐的模板编程可能会导致糟糕的意大利面代码,庞大的库,不良的功能隔离以及难以理解的编译时错误。
Erik Aronesty

5

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

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


您仍然可以通过不在标头中声明方法而只在实现中声明方法来隐藏方法,方法是使它们成为虚拟类中的非公共静态方法(可以从中赋予友谊,并且该方法适用于必须仅标头的模板) ),或将它们放在“详细信息”或类似名称的命名空间中。(这三种方法的工作方式都与传统的私有方法一样。)

我不明白您认为如何通过不在标头中声明来隐藏“向量”方法。它已经在向量中声明。
Jherico 2010年

耶赫里科(Jherico):您是在跟我说话还是斯季恩?无论哪种方式,我认为您都误解了我们中的一个。

@roger我是第二个Jherico,但我不认为我理解您:您是在谈论从std :: vector或其他方式隐藏方法吗?另外,如何将方法放在另一个名称空间中使其隐藏?只要在标头中声明任何人都可以访问它,就不会以私有关键字隐藏的方式真正隐藏吗?
stijn 2010年

stijn:这就是我指出的关于私有访问的内容,它也没有真正隐藏起来,因为任何有权访问标头的人都可以读取源代码或-Dprivate=public在编译器命令行上使用。像private这样的访问说明符大部分是文档,恰好是强制性的。

4

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


1

这样做更容易:

typedef std::vector<MyObject> MyContainer;

3
我了解,但我想做:typedef(std :: vector <MuObject> + mods)MyContainer简而言之。
Emile Cormier 2010年

1

无论如何,都将内联转发方法。这样您将无法获得更好的性能。实际上,您可能会获得较差的性能。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.