子类/继承标准容器?

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

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

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

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

虚拟dtors的基本规则

其次,这是非常糟糕的设计。 实际上有几个原因是设计不好。 首先,您应该始终通过一般运行的算法来扩展标准容器的功能。 这是一个简单的复杂性原因 – 如果你必须为每个容器编写一个算法,并且你有M个容器和N个算法,那么你必须编写M×N个方法。 如果你一般地写你的算法,你只有N个算法。 所以你得到更多的重用。

这也是非常糟糕的设计,因为你从容器中继承了一个好的封装。 一个好的经验法则是:如果您可以使用某个类型的公共接口来执行您所需的操作,则可以在该类型之外创建新的行为。 这改善了封装。 如果它是一个你想实现的新行为,把它作为一个命名空间范围函数(就像算法)。 如果你有一个新的不变强加,在一个类中使用遏制。

封装的经典描述

最后,总的来说,你不应该把继承看作延长类的行为的手段。 这是由于对重用的思考不清楚而产生的早期面向对象(OOP)理论的一个重大而不好的谎言,即使有一个明确的理论,它仍然被教导和提升到今天。 当你使用继承来扩展行为时,你将这种扩展的行为绑定到你的接口契约上,把用户的手联系到未来的变化。 例如,假设你有一个使用TCP协议进行通信的类型为Socket的类,并且通过从Socket中派生一个类SSLSocket并在Socket之上实现更高的SSL协议的行为来扩展它的行为。 现在,假设您有一个新的要求,即拥有相同的通信协议,但通过USB线或电话。 您需要将所有工作剪切并粘贴到来自USB类或Telephony类的新类。 而现在,如果你发现一个bug,你必须在三个地方修复它,这并不总是会发生,这意味着bug会花费更长的时间,并不总是固定的。

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

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

所有反对派生的动机都很弱。 派生与组合不同。 这只是一种“把东西放在一起”的方式。 构图把东西放在一起给它们命名,继承没有给出明确的名字。

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

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

继承它,并添加你所需要的(只是重写构造函数,直到C ++律师决定让它们也是可继承的:我还记得10年前关于“为什么ctors不能互相调用”的狂热讨论,为什么是一个“糟糕的坏事”……直到C ++ 11允许它,并且所有这些狂热分子突然闭嘴!),并且让新的析构函数与原来的不一样。

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

所有的“避免这个”“不这样做”总是听起来像是在本质上是不可知论的“道德化”。 一种语言的所有特征都可以解决一些问题。 事实上,解决问题的方法好坏取决于上下文,而不取决于特征本身。 如果你正在做的事情需要服务于许多容器,继承可能不是这样(你必须重做)。 如果是针对特定情况…继承是一种构成方式。 忘记OOP纯粹:C ++不是一个“纯粹的OOP”,容器根本不是OOP。

你应该避免从标准公认的公众中获得。 你可以在私人继承组合之间进行选择,在我看来,所有的一般准则都表明在这里组合更好,因为你没有重写任何函数。 不要公开形式的STL容器 – 实际上并不需要它。

顺便说一句,如果你想添加一堆算法到容器中,可以考虑把它们作为独立函数添加一个迭代器范围。

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

不得不写一些转发功能似乎是一个小的代价相比。

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

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

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

我偶尔从集合类型继承作为一种更好的方式来命名类型。
我不喜欢typedef作为个人喜好的事情。 所以我会做这样的事情:

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

那么写起来就容易多了:

 GizmoList aList = GetGizmos(); 

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

公开继承是其他人所说的所有原因的一个问题,也就是说你的容器可以被升级到没有虚拟析构函数或虚拟赋值运算符的基类,这会导致分割问题 。

另一方面,私下继承不是一个问题。 考虑下面的例子:

 #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; } 

OUTPUT:

 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 

恕我直言,我没有发现任何伤害继承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