子类/继承标准容器?

发布于 11-26 03:26 字数 149 浏览 0 评论 0原文

我经常在 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

滥情空心2024-12-03 03:26:26

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

首先,这是一个坏主意,因为标准容器没有虚拟析构函数。您永远不应该使用没有虚拟析构函数的多态对象,因为您无法保证派生类中的清理工作。

虚拟的基本规则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.

哥,最终变帅啦2024-12-03 03:26:26

也许这里的很多人不会喜欢这个答案,但现在是时候告诉一些异端邪说了,是的……还被告知“国王是赤身裸体的!”

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

如果您需要一个具有相同接口和 std::vector 实现的向量以及更多内容,您可以:

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

  • 继承它并添加您需要的内容(并且......只需重写构造函数,直到 C++ 律师决定让它们也可以继承:我仍然记得 10 年前狂热者讨论“为什么 ctor 不能互相调用”以及为什么它是一个“坏坏坏事”......直到 C++11 允许它,突然所有那些狂热者都闭嘴了!)并让新的析构函数像原来的析构函数一样是非虚拟。< /p>

就像每个具有 some virtual 方法而 some 没有的类一样,您知道您不能假装调用非virtual<通过基址寻址派生的 /code> 方法,同样适用于deletedelete 没有理由假装有任何特别的特别照顾。

知道任何非虚拟的东西都不能调用基址的程序员,也知道在分配派生后不要在基址上使用删除。

所有的“避免这个”、“不要那样做”,听起来总是像是对天生不可知论的事物的“道德化”。语言的所有功能都是为了解决某个问题而存在的。解决问题的给定方法的好坏取决于上下文,而不是功能本身。

如果您正在做的事情需要为许多容器提供服务,继承可能不是方法(您必须为所有容器重做)。如果是针对具体情况……继承是一种组合方式。忘掉 OOP 纯粹主义吧:C++ 不是“纯 OOP”语言,容器也根本不是 OOP。

Maybe many people here will not like this answer, but it is time for some heresy to be told and yes ... be told also that "the king is naked!"

All the motivation against the derivation are weak. Derivation is not different than composition. It's just a way to "put things together".
Composition puts things together giving them names, inheritance does it without giving explicit names.

If you need a vector that has the same interface and implementation of std::vector plus something more, you can:

  • use composition and rewrite all the embedded object function prototypes implementing function that delegates them (and if they are 10000... yes: be prepared to rewrite all those 10000) or...

  • inherit it and add just what you need (and ... just rewrite constructors, until C++ lawyers will decide to let them be inheritable as well: I still remember 10 year ago zealot discussion about "why ctors cannot call each other" and why it is a "bad bad bad thing" ... until C++11 permitted it and suddenly all those zealots shut up!) and let the new destructor be non-virtual as it was in the original one.

Just like for every class that has some virtual method and some not, you know you cannot pretend to invoke the non-virtual method of derived by addressing the base, the same applies for delete. There is no reason just for delete to pretend any particular special care.

A programmer who knows that whatever is not virtual isn't callable addressing the base, also knows not to use delete on your base after allocating your derived.

All the "avoid this", "don't do that", always sound as "moralization" of something that is natively agnostic. All the features of a language exist to solve some problem. The fact a given way to solve the problem is good or bad depends on the context, not on the feature itself.

If what you're doing needs to serve many containers, inheritance is probably not the way (you have to redo for all). If it is for a specific case ... inheritance is a way to compose. Forget OOP purisms: C++ is not a "pure OOP" language, and containers are not OOP at all.

情何以堪。2024-12-03 03:26:26

出于其他人所说的所有原因,公开继承是一个问题,即您的容器可以向上转换为没有虚拟析构函数或虚拟赋值运算符的基类,这可能会导致 切片问题

另一方面,私人继承则不是什么问题。考虑以下示例:

#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

Publicly inheriting is a problem for all the reasons others have stated, namely that your container can be upcasted to the base class which does not have a virtual destructor or virtual assignment operator, which can lead to slicing problems.

Privately inheriting, on the other hand, is less of an issue. Consider the following example:

#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

Clean and simple, and gives you the freedom to extend std containers without much effort.

And if you think about doing something silly, like this:

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

You get this:

error C2243: 'type cast': conversion from 'MyVector<int> *' to 'std::vector<T,std::allocator<_Ty>> *' exists, but is inaccessible
权谋诡计2024-12-03 03:26:26

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

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

You should refrain from deriving publicly from standard contianers. You may choose between private inheritance and composition and it seems to me that all the general guidelines indicate that composition is better here since you don't override any function. Don't derive publicly form STL containers - there really isn't any need of it.

By the way, if you want to add a bunch of algorithms to the container, consider adding them as freestanding functions taking an iterator range.

毁梦2024-12-03 03:26:26

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

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

The problem is that you, or someone else, might accidentally pass your extended class to a function expecting a reference to the base class. That will effectively (and silently!) slice off the extensions and create some hard to find bugs.

Having to write some forwarding functions seems like a small price to pay in comparison.

风吹雨成花2024-12-03 03:26:26

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

Because you can never guarantee that you haven't used them in a polymorphic way. You're begging for problems. Taking the effort to write a few functions is no big deal, and, well, even wanting to do this is dubious at best. What happened to encapsulation?

静若繁花2024-12-03 03:26:26

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

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

Most common reason to want to inherit from the containers is because you want to add some member function to the class. Since stdlib itself is not modifiable, inheritance is thought to be the substitute. This does not work however. It's better to do a free function that takes a vector as parameter:

void f(std::vector<int> &v) { ... }
海未深2024-12-03 03:26:26

恕我直言,如果将 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

这将完全禁止使用向量

IMHO, I don't find any harm in inheriting STL containers if they are used as functionality extensions. (That's why I asked this question. :) )

The potential problem can occur when you try to pass the pointer/reference of your custom container to a standard container.

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'

Such problems can be avoided consciously, provided good programming practice is followed.

If I want to take extreme care then I can go upto this:

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

Which will disallow using vector entirely.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文