关于继承:从C ++ STL容器派生是否存在任何真正的风险?

Is there any real risk to deriving from the C++ STL containers?

使用标准C++容器作为基类是错误的说法让我吃惊。

如果不是滥用语言来声明…

1
2
3
// Example A
typedef std::vector<double> Rates;
typedef std::vector<double> Charges;

…那么,确切地说,宣布…

1
2
3
4
5
6
7
// Example B
class Rates : public std::vector<double> {
    // ...
} ;
class Charges: public std::vector<double> {
    // ...
} ;

B的积极优势包括:

  • 启用函数重载,因为f(费率)和f(费用)是不同的签名
  • 使其他模板能够专门化,因为x和x是不同的类型
  • 转发声明是微不足道的
  • 调试器可能会告诉您对象是费率还是费用
  • 如果随着时间的推移,费率和收费会发展出个性化,那么费率和收费的输出格式就是一个单一的费率输出格式,这一功能的实现范围是显而易见的。

A的积极优势包括:

  • 不必提供构造函数等的简单实现
  • 15年前的标准编译器,这是唯一能编译你的遗产的东西,不会让你窒息。
  • 由于专业化是不可能的,模板x和模板x将使用相同的代码,因此不会出现无意义的膨胀。

这两种方法都优于使用原始容器,因为如果实现从vector更改为vector,那么只有一个地方可以用b更改,可能只有一个地方可以用a更改(可能更多,因为有人可能在多个地方放置了相同的typedef语句)。

我的目标是,这是一个具体的、可回答的问题,而不是讨论更好或更糟的实践。显示从标准容器派生可能会发生的最糟糕的事情,而使用typedef可以避免这种情况。

编辑:

毫无疑问,在类费率或类费用中添加析构函数是一种风险,因为std::vector不会将其析构函数声明为虚拟的。示例中不存在析构函数,也不需要析构函数。销毁Rates或Charges对象将调用基类析构函数。这里也不需要多态性。挑战是要显示出由于使用派生而不是typedef而导致的坏情况。

编辑:

考虑这个用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <vector>
#include <iostream>

void kill_it(std::vector<double> *victim) {
    // user code, knows nothing of Rates or Charges

    // invokes non-virtual ~std::vector<double>(), then frees the
    // memory allocated at address victim
    delete victim ;

}

typedef std::vector<double> Rates;
class Charges: public std::vector<double> { };

int main(int, char **) {
  std::vector<double> *p1, *p2;
  p1 = new Rates;
  p2 = new Charges;
  // ???  
  kill_it(p2);
  kill_it(p1);
  return 0;
}

有没有任何可能的错误,即使是一个任意倒霉的用户可以引入????哪个部分将导致费用(派生类)出现问题,而不是费率(typedef)出现问题?

在Microsoft实现中,vector本身是通过继承实现的。vector是由vector应该优先考虑遏制吗?


标准容器没有虚拟析构函数,因此不能以多态方式处理它们。如果你不愿意,而且每个使用你的代码的人都不愿意,这本身就不是"错误的"。不过,为了清晰起见,最好还是使用构图。


因为您需要一个虚拟析构函数,而std容器没有它。标准容器并不是设计为充当基类的。

有关更多信息,请阅读文章"为什么我们不从STL类继承类?"

指南基类必须具有:

  • 公共虚拟析构函数
  • 或受保护的非虚拟析构函数


在我看来,一个强有力的反驳理由是,您正在将接口和实现强加于您的类型上。当您发现向量内存分配策略不适合您的需要时会发生什么?你会从std:deque中得到吗?那些128K行已经使用了类的代码呢?每个人都需要重新编译所有东西吗?它还会编译吗?


这个问题不是一个低级的问题,而是一个实现问题。标准容器的析构函数不是虚拟的,这意味着没有办法在它们上面使用运行时多态性来获得正确的描述器。

我在实践中发现,仅使用我的代码需要定义的方法(以及"父"类的私有成员)创建自己的自定义列表类并没有那么痛苦。事实上,它常常会导致设计得更好的类。


除了基类需要一个虚拟析构函数或一个受保护的非虚拟析构函数之外,您还在设计中进行以下断言:

在上面的示例中,费率和收费与双精度向量相同。根据你自己的断言,"……随着时间的推移,利率和收费会发展成个性……"那么,在这一点上,利率仍然是双倍向量的一个断言吗?例如,双精度向量不是单精度向量,因此,如果我使用您的速率为小部件声明双精度向量,那么您的代码可能会让我头疼。关于费率和收费还有什么可能改变?是否有任何基本类的变更安全地与您设计的客户隔离?它们是否应该以一种基本的方式进行变更?

关键是一个类是C++中许多元素来表达设计意图的元素。说出你的意思和你所说的意思是反对以这种方式使用继承权的原因。

…或者只是在我的回答之前更简洁地发布了:替换。


而且,在大多数情况下,如果可能的话,您应该更喜欢组合或聚合而不是继承。


Is there any possible error that even an arbitrarily hapless user could introduce in the ??? section which will result in a problem with Charges (the derived class), but not with Rates (the typedef)?

首先,曼卡西有一个很好的观点:

The comment in kill_it is wrong. If the dynamic type of victim is not std::vector, then the delete simply invokes undefined behaviour. The call to kill_it(p2) causes this to happen, and so nothing needs to be added to the //??? section for this to have undefined behaviour. – Mankarse Sep 3 '11 at 10:53

其次,假设他们称之为f(*p1);,其中f专门用于std::vector:找不到vector专门化-最终可能会以不同方式匹配模板专门化-通常运行(较慢或效率较低)通用代码,或者如果实际上没有专门化的版本,则会出现链接器错误。被罚款的通常不是一个重要的问题。

就我个人而言,我认为通过指向基的指针进行破坏是越界的——考虑到您当前的编译器、编译器标志、程序、操作系统版本等,这可能只是一个"假设性"问题(就您所知),但它可能在任何时候因为"不好"的原因而中断。

如果您确信可以通过一个基类指针避免删除,那么就进行删除。

也就是说,关于您的评估的一些注释:

  • "为构造函数提供微不足道的实现"——这是一个麻烦,但是C++ 03的一个技巧:EDCOX1(4)有时比枚举所有重载更容易,但是不处理非EDCOX1、5个参数、默认值、显式与非显式构造函数,也不会缩放到大量的参数。C++ 11提供了一个更好的通用解决方案。

Without question, adding a destructor to class Rates or class Charges would be a risk, because std::vector does not declare its destructor as virtual. There is no destructor in the example, and no need for one. Destroying a Rates or Charges object will invoke the base class destructor. There is no need for polymorphism here, either.

  • 如果没有以多态方式删除对象,派生类析构函数不会带来风险;如果存在未定义的行为,则派生类是否具有用户定义的析构函数。也就是说,当您添加数据成员或具有执行清理的析构函数的进一步基(内存释放、互斥锁解锁、文件句柄关闭等)时,您从"可能是牛仔"过渡到"几乎肯定不是牛仔"。

  • 说"将调用基类析构函数"听起来像是直接完成的,不涉及隐式定义的派生类析构函数,或者调用——所有这些都是优化细节,而不是标准指定的。


一个词:可替代性