子类/继承标准容器?
我经常在 Stack Overflow 上读到这样的说法。就我个人而言,我认为这没有任何问题,除非我以多态方式使用它;即我必须使用虚拟析构函数。
如果我想扩展/添加标准容器的功能,那么有什么比继承更好的方法呢?将这些容器包装在自定义类中需要更多的努力,而且仍然不干净。
I often read this statements on Stack Overflow. Personally, I don't find any problem with this, unless I am using it in a polymorphic way; i.e. where I have to use virtual
destructor.
If I want to extend/add the functionality of a standard container then what is a better way than inheriting one? Wrapping those container inside a custom class requires much more effort and is still unclean.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
发布评论
评论(8)
也许这里的很多人不会喜欢这个答案,但现在是时候告诉一些异端邪说了,是的……还被告知“国王是赤身裸体的!”
所有反对推导的动机都很弱。派生与组合没有什么不同。这只是“将事物组合在一起”的一种方式。
组合将事物组合在一起并给它们命名,而继承则在不给出明确名称的情况下完成此操作。
如果您需要一个具有相同接口和 std::vector 实现的向量以及更多内容,您可以:
使用组合并重写所有嵌入的对象函数原型来实现委托它们的函数(如果它们是 10000...是的:准备重写所有这些 10000) 或...
继承它并添加您需要的内容(并且......只需重写构造函数,直到 C++ 律师决定让它们也可以继承:我仍然记得 10 年前狂热者讨论“为什么 ctor 不能互相调用”以及为什么它是一个“坏坏坏事”......直到 C++11 允许它,突然所有那些狂热者都闭嘴了!)并让新的析构函数像原来的析构函数一样是非
虚拟
。< /p>
就像每个具有 some virtual
方法而 some 没有的类一样,您知道您不能假装调用非virtual<通过基址寻址派生的 /code> 方法,同样适用于
delete
。 delete
没有理由假装有任何特别的特别照顾。
知道任何非虚拟的东西都不能调用基址的程序员,也知道在分配派生后不要在基址上使用删除。
所有的“避免这个”、“不要那样做”,听起来总是像是对天生不可知论的事物的“道德化”。语言的所有功能都是为了解决某个问题而存在的。解决问题的给定方法的好坏取决于上下文,而不是功能本身。
如果您正在做的事情需要为许多容器提供服务,继承可能不是方法(您必须为所有容器重做)。如果是针对具体情况……继承是一种组合方式。忘掉 OOP 纯粹主义吧:C++ 不是“纯 OOP”语言,容器也根本不是 OOP。
出于其他人所说的所有原因,公开继承是一个问题,即您的容器可以向上转换为没有虚拟析构函数或虚拟赋值运算符的基类,这可能会导致 切片问题。
另一方面,私人继承则不是什么问题。考虑以下示例:
#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 容器,而无需付出太多努力。
如果你想做一些愚蠢的事情,比如:
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 容器用作功能扩展,我认为继承 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
这将完全禁止使用向量
。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
有很多原因表明这是一个坏主意。
首先,这是一个坏主意,因为标准容器没有虚拟析构函数。您永远不应该使用没有虚拟析构函数的多态对象,因为您无法保证派生类中的清理工作。
虚拟的基本规则dtors
其次,这确实是一个糟糕的设计。事实上,这是糟糕的设计有几个原因。首先,您应该始终通过通用操作的算法来扩展标准容器的功能。这是一个简单的复杂性原因 - 如果您必须为其适用的每个容器编写一个算法,并且您有 M 个容器和 N 个算法,那么您必须编写 M x N 方法。如果您通用地编写算法,则只有 N 个算法。所以你会得到更多的重用。
这也是一个非常糟糕的设计,因为您通过从容器继承来破坏良好的封装。一个好的经验法则是:如果可以使用类型的公共接口执行所需的操作,请将新行为设置为该类型的外部。这改善了封装。如果它是您想要实现的新行为,请将其设为命名空间作用域函数(如算法)。如果要施加新的不变量,请在类中使用包含。
封装的经典描述
最后,一般来说,您永远不应该将继承视为扩展类行为的一种手段。这是早期 OOP 理论中最大的、最糟糕的谎言之一,它是由于对重用的不明确思考而产生的,并且直到今天,尽管有一个明确的理论解释为什么它会继续被教授和推广,但它仍然存在。很糟糕。当您使用继承来扩展行为时,您正在将该扩展行为与您的接口契约联系起来,从而将用户的双手与未来的更改联系起来。例如,假设您有一个使用 TCP 协议进行通信的 Socket 类型的类,并且您通过从 Socket 派生类 SSLSocket 并在 Socket 之上实现更高层 SSL 堆栈协议的行为来扩展它的行为。现在,假设您收到一个新要求,要求具有相同的通信协议,但通过 USB 线或电话进行。您需要将所有工作剪切并粘贴到从 USB 类或电话类派生的新类中。现在,如果你发现一个错误,你必须在所有三个地方修复它,这不会总是发生,这意味着错误将花费更长的时间并且并不总是得到修复......
这对于任何继承层次结构都是通用的 A-> ;B->C->... 当您想要在不属于基类 A 的对象上使用在派生类中扩展的行为(例如 B、C、..)时,您必须重新设计或你正在重复实施。这导致了非常单一的设计,以后很难改变(想想微软的MFC,或者他们的.NET,或者——好吧,他们经常犯这个错误)。相反,只要有可能,您几乎应该总是考虑通过组合进行扩展。当您考虑“开放/封闭原则”时,应该使用继承。您应该通过继承类拥有抽象基类和动态多态运行时,每个都将完整实现。层次结构不应该太深——几乎总是两个级别。仅当您有不同的动态类别用于需要区分类型安全的各种函数时,才使用两个以上。在这些情况下,请使用抽象基,直到具有实现的叶类。
There are a number of reasons why this a bad idea.
First, this is a bad idea because the standard containers do not have virtual destructors. You should never use something polymorphically that does not have virtual destructors, because you cannot guarantee cleanup in your derived class.
Basic rules for virtual dtors
Second, it is really bad design. And there are actually several reasons it is bad design. First, you should always extend the functionality of standard containers through algorithms that operate generically. This is a simple complexity reason - if you have to write an algorithm for every container it applies to and you have M containers and N algorithms, that is M x N methods you must write. If you write your algorithms generically, you have N algorithms only. So you get much more reuse.
It is also really bad design because you are breaking a good encapsulation by inheriting from the container. A good rule of thumb is: if you can perform what you need using the public interface of a type, make that new behavior external to the type. This improves encapsulation. If it's a new behavior you want to implement, make it a namespace scope function (like the algorithms). If you have a new invariant to impose, use containment in a class.
A classic description of encapsulation
Finally, in general, you should never think about inheritance as a means to extend the behavior of a class. This is one of the big, bad lies of early OOP theory that came about due to unclear thinking about reuse, and it continues to be taught and promoted to this day even though there is a clear theory why it is bad. When you use inheritance to extend behavior, you are tying that extended behavior to your interface contract in a way that ties users hands to future changes. For instance, say you have a class of type Socket that communicates using the TCP protocol and you extend it's behavior by deriving a class SSLSocket from Socket and implementing the behavior of the higher SSL stack protocol on top of Socket. Now, let's say you get a new requirement to have the same protocol of communications, but over a USB line, or over telephony. You would need to cut and paste all that work to a new class that derives from a USB class, or a Telephony class. And now, if you find a bug, you have to fix it in all three places, which won't always happen, which means bugs will take longer and not always get fixed...
This is general to any inheritance hierarchy A->B->C->... When you want to use the behaviors you've extended in derived classes, like B, C, .. on objects not of the base class A, you've got to redesign or you are duplicating implementation. This leads to very monolithic designs that are very hard to change down the road (think Microsoft's MFC, or their .NET, or - well, they make this mistake a lot). Instead, you should almost always think of extension through composition whenever possible. Inheritance should be used when you are thinking "Open / Closed Principle". You should have abstract base classes and dynamic polymorphism runtime through inherited class, each will full implementations. Hierarchies shouldn't be deep - almost always two levels. Only use more than two when you have different dynamic categories that go to a variety of functions that need that distinction for type safety. In those cases, use abstract bases until the leaf classes, which have the implementation.