从C ++ STL容器派生出来是否有真正的风险?

声称使用标准C ++容器作为基类是一个错误,这让我感到惊讶。

如果没有滥用语言来宣布…

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

那么,确切的说是…

 // Example B class Rates : public std::vector<double> { // ... } ; class Charges: public std::vector<double> { // ... } ; 

B的积极优势包括:

  • 启用函数的重载,因为f(Rates&)和f(Charges&)是不同的签名
  • 使其他模板成为专用的,因为X <费率>和X <费用>是不同的types
  • 前向声明是微不足道的
  • debugging器可能会告诉你该对象是一个费率还是费用
  • 如果随着时间的推移,利率和收费发展个性化 – 一个单一的利率,一个收费的输出格式 – 这个function有一个明显的实施范围。

A的积极优势包括:

  • 不必提供构造函数的简单实现等
  • 这个十五年前的标准编译器是唯一能够编译你的遗产的编译器,不会窒息
  • 由于专业化是不可能的,所以模板X <费率>和模板X <费用>将使用相同的代码,所以没有无意义的膨胀。

这两种方法都优于使用原始容器,因为如果实现从vector <double>更改为vector <float>,则只有一个地方可以用B来更改, 也许只有一个地方可以用A来更改(可能更多,因为有人可能会在多个地方放置相同的typedef语句)。

我的目标是,这是一个具体的,可回答的问题,而不是讨论更好或更差的做法。 显示从标准容器派生的结果可能发生的最糟糕的事情,而使用typedef可以防止这种事情发生。

编辑:

毫无疑问,为类或费率类添加一个析构函数会是一个风险,因为std :: vector不会将其析构函数声明为虚函数。 例子中没有析构函数,也没有必要。 销毁汇率或收费对象将调用基类析构函数。 这里也不需要多态性。 面临的挑战是使用派生而不是typedef来显示不好的事情。

编辑:

考虑这个用例:

 #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 <T>本身是通过inheritance来实现的。 vector<T,A>是从_Vector_Val <T,A>公开派生的。是否应该包含首选?

标准容器不具有虚拟析构函数,因此不能多形地处理它们。 如果你不这样做,而且每个使用你的代码的人都没有,那本身就不是“错误的”。 然而,为了清晰起见,无论如何,你最好使用合成。

因为你需要一个虚拟析构函数,而std容器没有它。 std容器不是作为基类来devise的。

有关更多信息,请阅读文章“为什么我们不应该从STL类inheritance类?”

指南

基类必须具有:

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

在我看来,一个强有力的反驳论点是,你正在将一个接口实现强加到你的types上。 当你发现vector内存分配策略不适合你的需求时会发生什么? 你会从std:deque派生吗? 那些已经在使用你的课程的128K代码行呢? 每个人都需要重新编译一切吗? 它会甚至编译?

这个问题不是一个哲学问题,而是一个实现问题。 标准容器的析构函数不是虚拟的,这意味着无法使用运行时多态来获得合适的析构函数。

我在实践中发现,使用我的代码需要定义的方法(以及“父”类的私有成员)来创build我自己的自定义列表类并不是那么痛苦。 事实上,这往往导致更好的devise类。

除了基类需要一个虚拟析构函数或者一个受保护的非虚拟析构函数之外,你在你的devise中做了如下的断言:

利率和收费, 就像上面例子中的双打vector一样。 按照你自己的说法,“随着时间的推移,利率和收费发展个性…”,那么在这一点上,利率仍然是一个双打的vector的说法呢? 一个双打的vector不是一个单例,因此如果我使用你的价格来声明我的双向的Widgetsvector,我可能会从你的代码中产生一些头痛。 价格和收费有什么变化? 如果基本方法发生变化,是否有任何基类更改与您devise的客户端安全隔离?

关键是一个类是C ++中的许多元素,用来expressiondevise意图。 说出你的意思,并且说出你所说的是以这种方式反对inheritance的理由。

……或者在我的回答之前,更简洁地贴出来:换人。

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

一个字: 可替代性

有没有任何可能的错误,即使一个任意倒霉的用户可以介绍的??? 部分会导致收费(派生类)的问题,但与费率(typedef)?

首先,Mankarse的优点是:

kill_it的注释是错误的。 如果受害者的dynamictypes不是std::vector ,那么delete调用未定义的行为。 对kill_it(p2)的调用导致这种情况发生,所以没有什么需要添加到//??? 部分为此有未定义的行为。 – Mankarse 09年 9月3日在10:53

其次,说他们叫f(*p1); 其中f是专门为std::vector<double> :该vector专门化将不会被发现 – 您可能会不同地匹配模板专业化 – 通常运行(较慢或否则效率较低)通用代码,或者如果获得链接器错误if一个非专业版本实际上没有定义。 不经常是一个重大的关注。

就我个人而言,我认为通过一个指向基地的指针来破坏行为 – 在目前的编译器,编译器标志,程序,操作系统版本等方面,它可能只是一个“假设”的问题(就你所知)可能会随时打破,没有“好”的理由。

如果您确信可以避免通过基类指针进行删除,那就去做吧。

这就是说,关于你评估的一些说明:

  • “提供构造函数的简单实现” – 这是一个麻烦,但一个技巧03: template <typename A> Classname(const A& a) : Base(a) { } template <typename A, typename B> Classname(const A& a, const B& b) : Base(a, b) { } ...有时比列举所有重载更容易,但不处理非常量参数,默认值,显式与非显式构造函数也没有规模庞大的论据。 C ++ 11提供了一个更好的通用解决scheme。

毫无疑问,为class Ratesclass Rates class Charges添加一个析构函数会是一个风险,因为std::vector不会将其析构函数声明为虚函数。 例子中没有析构函数,也没有必要。 销毁汇率或收费对象将调用基类析构函数。 这里也不需要多态性。

  • 如果对象不是多态删除的, 派生类析构函数没有风险; 如果存在未定义的行为,无论派生类是否具有用户定义的析构函数。 也就是说,当你添加数据成员或者进一步使用清理的析构函数(内存释放,互斥解锁,文件释放)​​时,你从“可能为好的牛仔”变为“几乎肯定不行”手柄closures等)

  • 说“将调用基类析构函数”使得它听起来像直接完成没有隐式定义的派生类析构函数涉及或进行调用 – 所有的优化细节和标准没有指定。