子类/继承标准容器?

iam*_*ind 48 c++ inheritance standard-library

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

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

ex0*_*du5 82

这有一个坏主意有很多原因.

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

虚拟dtors的基本规则

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

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

封装的经典描述

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

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

  • +1:组合>继承 (9认同)
  • 关于继承的非常好的观点. (2认同)
  • ++++++++++++ 1.我非常喜欢的部分:1)通用和正交设计; 2)如果可以通过公共接口完成任务,请保持外部; 3)套接字/ SSL/USB示例 - 我已经看到很多遗留代码落入此陷阱 (2认同)

Emi*_*lia 27

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

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

如果你需要一个具有相同接口和std :: vect实现的向量以及更多东西,你可以:

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

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

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

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

  • @ybungalobil:你尝试过这两种方法吗?我怀疑你所说的只是别人告诉你要做的事,但你从未做过。独立函数是可以的,除非您不必在它们之间共享“状态”(毕竟,这就是类的用途)。 (3认同)
  • +1碗新鲜空气.反对这样做的唯一真正的论点是std lib被设计破坏了. (3认同)
  • 除非您有新的不变权来强制执行,否则不应使用合成.但是,你必须编写所有这些接口方法(因为你有一个新的不变量来强制执行!).如果您没有新的不变量,则命名空间作用域自由函数是完全合适的,并且比继承更好,因为它可以在多个类型上一般地工作.这是客观的重用,而不是意见.我知道没有人说打电话给ctors是坏的 - 这不可能用语言完成,这完全不同.没有这种道德,它是重用的客观指标. (2认同)
  • 我不同意 OOP继承和OOP组成与C ++继承,C ++成员是不同的概念。我可以用C ++成员实现对象继承,因为我可以用C ++继承实现对象组合。由于语言历史的缘故,C ++继承主要用于实现对象继承,而C ++成员主要用于实现对象组合这一事实,只是一个惯例。对象重用是一个与OOP抽象紧密相关的概念。代码重用是一个与语言“打字”紧密相关的概念。两者不一定必须相同。 (2认同)
  • @ybungalobil:那么问题是什么?OP明确表示延长。如果它没有新的附加状态可以在新方法之间共享,则不是“扩展”(恕我直言)。因此,如果您必须扩展,您可以嵌入并重写整套函数,或者继承最里面的函数并添加最外面的函数。这里不需要多态性。所以不要“挣扎”:只需导出并保存您的“重新输入所有 121 个 std::string 公共方法”,任何人都会感谢您。每个人都知道“yourstring”与其基础不是多态的。您在这里遇到了什么“维护问题”? (2认同)

Arm*_*yan 7

你应该避免公开从标准的contianers.您可以选择私有继承组合,在我看来,所有通用指南都表明组合在这里更好,因为您不会覆盖任何功能.不要公开形成STL容器 - 实际上并不需要它.

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

  • 对我来说,"真的没有任何需要"的论点是一个非论证.对不起;).你没有真正解释为什么不应该这样做,技术原因是什么,而不仅仅是说"这是一种不好的做法,因为这是一种不好的做法" (17认同)
  • @iammilind:是的,你可以,但组成应该是首选.有关收容与非公共继承的讨论,请参见http://www.gotw.ca/publications/mill06.htm (2认同)

Ers*_*oat 6

由于其他人陈述的所有原因,公开继承是一个问题,即,您的容器可以上载到没有虚拟析构函数或虚拟赋值运算符的基类,这可能导致切片问题

另一方面,私有继承问题不大。考虑以下示例:

#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;
}
Run Code Online (Sandbox Code Playgroud)

输出:

1
2
3
4
5
6
7
8
9
10
10
9
8
7
6
5
4
3
2
1
Run Code Online (Sandbox Code Playgroud)

干净简单,让您无需花费太多精力即可自由扩展std容器。

如果您考虑做一些愚蠢的事情,例如:

std::vector<int>* stdVector = &intArray;
Run Code Online (Sandbox Code Playgroud)

你得到这个:

error C2243: 'type cast': conversion from 'MyVector<int> *' to 'std::vector<T,std::allocator<_Ty>> *' exists, but is inaccessible
Run Code Online (Sandbox Code Playgroud)


Bo *_*son 5

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

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

  • @iammilind:如果您私有继承,则无法将继承的类传递给所述函数。基类将无法访问。- 私有继承的类*不是*基类,继承只是一个实现细节。 (4认同)