关于C++:子类/继承标准容器?

Subclass/inherit standard containers?

我经常在堆栈溢出时阅读这些语句。就我个人而言,我没有发现任何问题,除非我以多态的方式使用它;也就是说,我必须使用virtual析构函数。

如果我想扩展/添加标准容器的功能,那么什么方法比继承一个更好呢?在自定义类中包装这些容器需要付出更多的努力,而且仍然是不干净的。


这是个坏主意有很多原因。

首先,这是一个坏主意,因为标准容器没有虚拟析构函数。您不应该使用没有虚拟析构函数的多态性,因为您不能保证在派生类中进行清理。

虚拟数据终端的基本规则

第二,它的设计很糟糕。事实上,它的设计不好有几个原因。首先,您应该始终通过一般操作的算法扩展标准容器的功能。这是一个简单的复杂性原因——如果您必须为它应用的每个容器编写一个算法,并且您有m个容器和n个算法,那就是您必须编写的m x n方法。如果您一般地编写算法,那么您只有n个算法。所以你可以得到更多的重用。

它也是非常糟糕的设计,因为您正通过从容器继承来破坏一个好的封装。一个好的经验法则是:如果您可以使用一个类型的公共接口来执行您所需要的操作,那么就将这个新行为设置为该类型的外部行为。这改进了封装。如果这是您想要实现的新行为,请将其设置为名称空间范围函数(如算法)。如果您有一个新的不变量要强制使用,请在类中使用包含。

封装的经典描述

最后,一般来说,您不应该将继承看作扩展类行为的一种方法。这是早期OOP理论中的一个大的、不好的谎言,它是由于对重用的不清楚的思考而产生的,尽管有一个明确的理论为什么它是不好的,但它仍然被教授和推广到今天。当您使用继承来扩展行为时,您正以一种将用户的手与将来的更改联系起来的方式将扩展行为与您的接口契约联系起来。例如,假设您有一个类型为socket的类,它使用TCP协议进行通信,并且通过从socket派生一个类ssl socket并在socket上实现更高的ssl堆栈协议的行为来扩展它的行为。现在,假设您得到了一个新的要求,拥有相同的通信协议,但是通过USB线或电话。您需要将所有工作剪切并粘贴到从USB类或电话类派生的新类中。现在,如果你发现了一个bug,你必须在所有三个地方修复它,这不会总是发生,这意味着bug将花费更长的时间,而不是总是得到修复…

这是任何继承层次结构A->B->C->当您想要使用在派生类(如B、C等)中扩展的行为时。对于不属于基类A的对象,必须重新设计或复制实现。这导致了非常难以改变的整体设计(想想微软的MFC,或者他们的.NET,或者-嗯,他们犯了很多错误)。相反,只要有可能,您几乎应该总是考虑通过组合进行扩展。当您认为"开放/封闭原则"时,应该使用继承。通过继承类,您应该有抽象的基类和动态多态运行时,每个都将完整实现。层次结构不应该很深——几乎总是两个层次。只有当您有不同的动态类别,这些类别涉及到需要区分类型安全性的各种功能时,才使用两个以上的类别。在这些情况下,使用抽象基直到叶类,叶类具有实现。


也许这里的很多人都不喜欢这个答案,但现在是时候告诉一些异端邪说了,是的……又有人说:"王是赤身露体的!"

所有反对推导的动机都很弱。派生与组成没有区别。这只是一种"把事情放在一起"的方式。组合将事物组合在一起,赋予它们名称,继承则不提供明确的名称。

如果您需要一个具有相同接口和std::vect实现的向量,再加上其他一些,您可以:

使用组合并重写所有嵌入的对象函数原型,实现委托它们的函数(如果它们是10000…是的:准备重写所有的10000)或者……

继承它并添加您需要的内容(并且…只是改写构造函数,直到C++的律师也会决定让它们也可以继承:我仍然记得10年前的狂热讨论"为什么Ccor不能互相调用",以及为什么它是一个"坏坏坏东西"…直到C++ 11允许它,突然所有狂热分子闭嘴!让新的析构函数非虚化,就像原来的那样。

就像每个类都有一些虚方法,而有些没有,你知道你不能假装通过寻址基来调用派生的非虚方法,这同样适用于删除。没有理由只删除就假装特别小心。一个知道什么不是虚拟的,不能调用的程序员也知道在分配派生的之后不要在您的基上使用delete。

所有的"避免这件事""不要做那件事"总是听起来像"道德化"的东西是天生的不可知论者。一种语言的所有特征都存在,以解决某些问题。给定的解决问题的方法是好是坏取决于上下文,而不是取决于特性本身。如果您所做的工作需要为多个容器提供服务,那么继承可能不是一种方法(您必须为所有容器重做)。如果是针对一个特定的案例…继承是一种组合方式。忘记OOP纯化:C++不是一个"纯OOP",容器根本不是OOP。


你应该避免公开地从标准的继承人那里派生出来。您可以在私有继承和组合之间进行选择,在我看来,所有的一般准则都表明这里的组合更好,因为您不重写任何函数。不要从STL容器中公开派生-实际上没有任何需求。

顺便说一下,如果您想向容器中添加一组算法,可以考虑将它们作为采用迭代器范围的独立函数进行添加。


问题是,您或其他人可能会意外地将扩展类传递给期望引用基类的函数。那将有效地(并且安静地!)切掉扩展并创建一些难以发现的错误。

相比之下,写一些转发功能似乎是一个很小的代价。


公共继承是一个问题,因为其他人已经说过所有的原因,也就是说,您的容器可以被升级到没有虚拟析构函数或虚拟分配运算符的基类,这可能导致切片问题。

另一方面,私人继承并不是一个问题。请考虑以下示例:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <vector>
#include <iostream>

// private inheritance, nobody else knows about the inheritance, so nobody is upcasting my
// container to a std::vector
template <class T> class MyVector : private std::vector<T>
{
private:
    // in case I changed to boost or something later, I don't have to update everything below
    typedef std::vector<T> base_vector;

public:
    typedef typename base_vector::size_type       size_type;
    typedef typename base_vector::iterator        iterator;
    typedef typename base_vector::const_iterator  const_iterator;

    using base_vector::operator[];

    using base_vector::begin;
    using base_vector::clear;
    using base_vector::end;
    using base_vector::erase;
    using base_vector::push_back;
    using base_vector::reserve;
    using base_vector::resize;
    using base_vector::size;

    // custom extension
    void reverse()
    {
        std::reverse(this->begin(), this->end());
    }
    void print_to_console()
    {
        for (auto it = this->begin(); it != this->end(); ++it)
        {
            std::cout << *it << '
'
;
        }
    }
};


int main(int argc, char** argv)
{
    MyVector<int> intArray;
    intArray.resize(10);
    for (int i = 0; i < 10; ++i)
    {
        intArray[i] = i + 1;
    }
    intArray.print_to_console();
    intArray.reverse();
    intArray.print_to_console();

    for (auto it = intArray.begin(); it != intArray.end();)
    {
        it = intArray.erase(it);
    }
    intArray.print_to_console();

    return 0;
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1
2
3
4
5
6
7
8
9
10
10
9
8
7
6
5
4
3
2
1

干净和简单,并给你自由扩展性病容器没有太多的努力。

如果你想做些傻事,比如:

1
std::vector<int>* stdVector = &intArray;

你得到这个:

1
error C2243: 'type cast': conversion from 'MyVector<int> *' to 'std::vector<T,std::allocator<_Ty>> *' exists, but is inaccessible

想要从容器继承的最常见原因是您想要向类中添加一些成员函数。因为stdlib本身是不可修改的,所以继承被认为是替代的。但这不起作用。最好是做一个以向量为参数的自由函数:

1
void f(std::vector<int> &v) { ... }


因为你永远不能保证你没有以多态的方式使用它们。你在乞求问题。花点精力写几个函数没什么大不了的,而且,好吧,即使想要这样做,充其量也是值得怀疑的。封装发生了什么?


我偶尔从集合类型继承,只是作为命名类型的更好方法。我不喜欢把typedef作为个人喜好。所以我会做如下的事情:

1
2
3
4
class GizmoList : public std::vector<CGizmo>
{
    /* No Body & no changes.  Just a more descriptive name */
};

这样写起来更容易、更清晰:

1
GizmoList aList = GetGizmos();

如果你开始向Gizmolist添加方法,你很可能会遇到麻烦。


imho,如果STL容器被用作功能扩展,那么继承它不会有任何危害。(这就是我问这个问题的原因。:)

当您试图将自定义容器的指针/引用传递到标准容器时,可能会发生潜在的问题。

1
2
3
4
5
6
template<typename T>
struct MyVector : std::vector<T> {};

std::vector<int>* p = new MyVector<int>;
//....
delete p; // oops"Undefined Behavior"; as vector::~vector() is not 'virtual'

只要遵循良好的编程实践,就可以有意识地避免这些问题。

如果我想非常小心,那么我可以继续:

1
2
3
4
#include<vector>
template<typename T>
struct MyVector : std::vector<T> {};
#define vector DONT_USE

完全不允许使用vector