从C ++ STL容器中获取是否存在任何真正的风险?

时间:2009-05-28 17:42:34

标签: c++ inheritance 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(费率&amp;)和f(费用&amp;)是不同的签名
  • 允许其他模板专用,因为X&lt; Rates&gt;和X&lt; Charges&gt;是不同的类型
  • 前瞻性声明很简单
  • 调试器可能会告诉您对象是费率还是费用
  • 如果随着时间的推移,费率和费用会产生个性 - 一种费率的单身人士,费用的输出格式 - 这个功能有明显的实施范围。

A的积极优势包括:

  • 不必提供构造函数等的简单实现
  • 十五年前的预标准编译器是唯一可以编译遗产的编译器
  • 由于不可能进行专业化,因此模板X&lt; Rate&gt;和模板X&lt;费用&gt;将使用相同的代码,所以没有毫无意义的膨胀。

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

我的目标是,这是一个具体的,可回答的问题,而不是对更好或更差实践的讨论。显示由于从标准容器派生而可能发生的最糟糕的事情,这可以通过使用typedef来防止。

编辑:

毫无疑问,向类Rate或类Charges添加析构函数会有风险,因为std :: vector不会将其析构函数声明为virtual。示例中没有析构函数,也不需要析构函数。销毁Rates或Charges对象将调用基类析构函数。这里也不需要多态性。挑战在于使用派生而不是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;
}

是否有任何可能的错误,甚至一个任意不幸的用户可能会在???哪个部分会导致Charges(派生类)出现问题,但不会导致Rate(typedef)?

在Microsoft实现中,向量&lt; T&gt;本身是通过继承实现的。矢量&lt; T,A&GT;是公开衍生自_Vector_Val&lt; T,A&gt;应该遏制是首选吗?

8 个答案:

答案 0 :(得分:25)

标准容器没有虚拟析构函数,因此您无法以多态方式处理它们。如果你不这样做,并且每个使用你代码的人都不这样做,那本身就不是“错误的”。但是,为了清晰起见,最好还是使用合成。

答案 1 :(得分:16)

因为你需要一个虚拟析构函数而std容器没有它。 std容器不能用作基类。

有关详情,请参阅文章"Why shouldn't we inherit a class from STL classes?"

<强>准则

基类必须具有:

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

答案 2 :(得分:6)

在我看来,一个强有力的反驳是你在你的类型上强加了一个接口。当您发现向量内存分配策略不符合您的需求时会发生什么?你会从std:deque派生出来吗?那些已经使用你的类的128K代码行怎么样?每个人都需要重新编译一切吗?它会编译吗?

答案 3 :(得分:5)

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

我在实践中发现,使用我的代码需要定义的方法(以及“父”类的私有成员)创建我自己的自定义列表类并不是那么痛苦。事实上,它通常会带来设计更好的课程。

答案 4 :(得分:3)

此外,在大多数情况下,如果可能,您应该优先选择合成或聚合而不是继承。

答案 5 :(得分:3)

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

费率和费用,与上面示例中的双打矢量相同。根据你自己的断言“...随着时间的推移,费率和收费会产生个性......”然后是说明利率仍然是相同的此时双倍的载体?例如,双向量不是单例,因此如果我使用你的比率来声明我的小部件的双向量,我可能会从你的代码中引起一些麻烦。关于费率和收费还有哪些变化?如果它们的基本方式有所改变,那么任何基类更改是否可以安全地与您设计的客户保持隔离?

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

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

答案 6 :(得分:1)

一个字:Substitutability

答案 7 :(得分:1)

  

是否有任何可能的错误,甚至一个任意不幸的用户可能会在???哪个部分会导致Charges(派生类)出现问题,但不会导致Rate(typedef)?

首先,Mankarse的优点是:

  

kill_it中的评论错误。如果受害者的动态类型不是std::vector,那么delete只会调用未定义的行为。对kill_it(p2)的调用会导致这种情况发生,因此不需要向//???部分添加任何内容以使其具有未定义的行为。 - Mankarse Sep 3 '11 at 10:53

其次,说他们致电f(*p1); f专门针对std::vector<double>vector专业化将无法找到 - 您最终可能会以不同的方式匹配模板专精化 - 通常运行(速度较慢或效率较低)通用代码,或者如果未实际定义非专用版本则会出现链接器错误。 通常不是一个重要问题。

就我个人而言,我认为通过一个指向base的指针进行破坏是为了跨越这条线 - 鉴于你当前的编译器,编译器标志,可能只是一个“假设的”问题(据你所知) ,程序,操作系统版本等 - 但它可以随时打破,没有“好”的理由。

如果您确信可以通过基类指针避免删除,请选择它。

那就是说,关于你的评估的一些注释:

  • “提供构造函数的简单实现” - 这是一个麻烦,但C ++ 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) { } ...有时比枚举所有重载更容易,但不处理非 - {{1}参数,默认值,显式vs非显式构造函数,也不扩展到大量参数。 C ++ 11提供了更好的通用解决方案。
  

毫无疑问,向constclass Rates添加析构函数会带来风险,因为class Charges并未将其析构函数声明为虚拟。示例中没有析构函数,也不需要析构函数。销毁Rates或Charges对象将调用基类析构函数。这里也不需要多态性。

  • 如果对象未被多态删除,则派生类析构函数不会带来风险;如果它是未定义的行为,无论您的派生类是否具有用户定义的析构函数。也就是说,当您使用执行清理的析构函数添加数据成员或其他基础时,您可以从“可能为牛仔”转为“几乎肯定不是正常”(内存释放,互斥锁解锁,文件)处理结束等。)

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