子类/继承标准容器?

时间:2011-07-24 10:06:53

标签: c++ standard-library inheritance

我经常在Stack Overflow上阅读这些语句。就个人而言,我没有发现任何问题,除非我以多态方式使用它;即我必须使用virtual析构函数。

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

9 个答案:

答案 0 :(得分:81)

为什么这是一个坏主意有很多原因。

首先,这是个坏主意,因为标准容器没有虚拟析构函数。你不应该使用没有虚拟析构函数的多态的东西,因为你无法保证派生类的清理。

Basic rules for virtual dtors

其次,这是非常糟糕的设计。实际上有几个原因是糟糕的设计。首先,您应该始终通过一般操作的算法扩展标准容器的功能。这是一个简单的复杂性原因 - 如果你必须为它应用的每个容器编写一个算法,你有M个容器和N个算法,那就是你必须编写的M x N方法。如果您通常编写算法,则只能使用N算法。所以你可以获得更多的重用。

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

A classic description of encapsulation

最后,一般来说,您永远不应该将继承视为扩展类行为的手段。这是由于对重用的思考不清晰而导致的早期OOP理论重大坏处之一之一,并且即使有明确的理论为什么它仍在继续教授和推广到今天。不好。当您使用继承来扩展行为时,您将这种扩展行为绑定到您的接口契约,以便将用户的手与未来的更改联系起来。例如,假设您有一个类型为Socket的类,它使用TCP协议进行通信,并通过从Socket派生类SSLSocket并在Socket之上实现更高SSL堆栈协议的行为来扩展它的行为。现在,假设你有一个新的要求,即通过USB线或通过电话获得相同的通信协议。您需要将所有工作剪切并粘贴到从USB类或Telephony类派生的新类中。现在,如果你发现了一个bug,你必须在所有三个地方修复它,这并不总是会发生,这意味着错误会花费更长的时间并且不会总是得到修复......

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

答案 1 :(得分:25)

可能很多人在这里不会喜欢这个答案,但现在是时候让一些异端被告知,是的......也被告知“国王是赤身裸体的!”

反对推导的所有动机都很弱。推导与构成没有什么不同。这只是“把事情放在一起”的一种方式。 组合将事物放在一起为它们命名,继承是在没有给出明确名称的情况下完成的。

如果你需要一个具有相同界面和std :: vect实现的矢量加上更多东西,你可以:

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

继承它并添加你需要的东西(并且......只重写构造函数,直到C ++律师将决定让它们也可以继承:我还记得10年前狂热者讨论“为什么ctors不能互相称呼”为什么它是一个“坏坏的坏事”......直到C ++ 11允许它并突然所有那些狂热者闭嘴!)并让新的析构函数不是虚拟的,因为它是原始的。

就像每个具有某些虚拟方法和某些的类都没有,你知道你不能假装通过寻址基数来调用派生的非虚方法,同样适用于删除。删除没有理由假装任何特殊的护理。 一个程序员知道什么不是虚拟的不可调用基地也知道在分配你的派生后你不会在你的基础上使用删除。

所有“避免这种情况”“不要这样做”总是听起来像是一种本能不可知的“道德化”。存在语言的所有特征以解决某些问题。解决问题的一种方法是好还是坏取决于上下文,而不是取决于特征本身。 如果您正在做的事情需要为许多容器提供服务,那么继承可能不是那种方式(您必须为所有人重做)。如果是针对特定情况......继承是一种撰写方式。忘记OOP纯粹主义:C ++不是一个“纯OOP”,容器根本就不是OOP。

答案 2 :(得分:6)

你应该避免公开从标准的contianers。您可以选择 私有继承 组合 ,在我看来,所有一般指导原则都表明组成是这里更好,因为你没有覆盖任何功能。 不公开派生STL容器 - 实际上并不需要它。

顺便说一句,如果你想在容器中添加一堆算法,可以考虑将它们添加为采用迭代器范围的独立函数。

答案 3 :(得分:4)

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

相比之下,必须编写一些转发函数似乎是一个很小的代价。

答案 4 :(得分:4)

公开继承是其他人所说的所有原因的问题,即你的容器可以被升级到没有虚拟析构函数或虚拟赋值运算符的基类,这可以导致slicing problems。 / p> 另一方面,私人继承不是一个问题。请考虑以下示例:

#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 << '\n';
        }
    }
};


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

干净简单,让您可以自由地扩展标准容器。

如果你想做一些愚蠢的事情,就像这样:

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

你明白了:

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

答案 5 :(得分:2)

因为你无法保证你没有以多态方式使用它们。你在乞求问题。努力编写一些函数并不是什么大问题,而且,即使想要这样做也是可疑的。封装发生了什么?

答案 6 :(得分:2)

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

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

答案 7 :(得分:-2)

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

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

然后写起来更容易,更清晰:

GizmoList aList = GetGizmos();

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

答案 8 :(得分:-3)

恕我直言,如果将STL容器用作功能扩展,我认为继承STL容器没有任何损害。 (这就是我问这个问题的原因。:))

当您尝试将自定义容器的指针/引用传递给标准容器时,可能会出现潜在问题。

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'

如果遵循良好的编程习惯,有意识地可以避免这些问题。

如果我想采取极度关怀那么我可以做到这一点:

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

这将完全禁止使用vector