代码之家  ›  专栏  ›  技术社区  ›  Thomas L Holaday

是否有来自C++ STL容器的实际风险?

  •  30
  • Thomas L Holaday  · 技术社区  · 15 年前

    使用标准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<费用>是不同的类型
    • 转发声明很简单
    • 调试器可能会告诉您对象是rates还是charges
    • 如果随着时间的推移,费率和收费会发展出个性——费率是单一的,收费是一种输出格式——那么这项功能显然还有实现的余地。

    A的积极优势包括:

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

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

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

    编辑:

    毫无疑问,将析构函数添加到类速率或类的收费中是一个风险,因为STD::vector并没有声明析构函数是虚的。本例中没有析构函数,也不需要析构函数。销毁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;
    }
    

    是否有任何可能的错误,即使是一个任意不幸的用户可以介绍在????会导致费用(派生类)而不是费率(typedef)问题的节?

    在Microsoft实现中,vector<t>本身是通过继承实现的。vector<t,a>是从vector<t,a>公开派生的,是否应首选包含?

    8 回复  |  直到 8 年前
        1
  •  25
  •   rlbond    15 年前

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

        2
  •  16
  •   TimW    15 年前

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

    有关更多信息,请阅读文章 "Why shouldn't we inherit a class from STL classes?"

    指南

    基类必须具有:

    • 公共虚拟析构函数
    • 或受保护的非虚拟析构函数
        3
  •  6
  •   Nikolai Fetissov    15 年前

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

        4
  •  5
  •   T.E.D.    15 年前

    这不是一个庸俗的问题,而是一个实现问题。标准容器的析构函数不是虚拟的,这意味着无法对它们使用运行时多态性来获得正确的析构函数。

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

        5
  •  3
  •   C Johnson    15 年前

    除了基类需要虚拟析构函数或受保护的非虚拟析构函数之外,您还在设计中声明以下内容:

    价格和费用, 上面例子中的双精度向量。根据你自己的断言“……随着时间的推移,费率和收费会发展出个性……”那么,费率的断言就是 还是一样的 在这一点上是双倍的向量?例如,doubles的向量不是singleton,因此如果我使用您的速率来声明小部件的doubles向量,我可能会因为您的代码而头疼。价格和收费还有什么变动吗?是否有任何基类更改与您的设计的客户端安全隔离?它们是否应该以基本方式更改?

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

    …或者在我的回复之前更简洁地发布:替换。

        6
  •  3
  •   Stefan Weiser    8 年前

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

        7
  •  1
  •   Johann Gerell    15 年前
        8
  •  1
  •   Community CDub    7 年前

    是否有任何可能的错误,即使是一个任意不幸的用户可以介绍在????会导致费用(派生类)而不是费率(typedef)问题的节?

    首先,曼卡塞的优点是:

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

    其次,说他们打电话 f(*p1); 哪里 f 是专门为 std::vector<double> vector 找不到专门化-您可能会以不同的方式匹配模板专门化-通常运行(较慢或效率较低)通用代码,或者在未实际定义专门化版本时出现链接器错误。 通常不是一个重要的问题。

    就我个人而言,我认为通过一个指向底部的指针来进行破坏是越界的。 可以 考虑到当前的编译器、编译器标志、程序、操作系统版本等,这只是一个“假想”的问题(据你所知),但它随时都有可能因为没有“好”的理由而崩溃。

    如果您确信可以避免通过基类指针进行删除,请继续。

    也就是说,关于您的评估,请注意以下几点:

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

    毫无疑问,添加析构函数到 class Rates class Charges 会有风险,因为 STD::载体 不将其析构函数声明为虚拟的。本例中没有析构函数,也不需要析构函数。销毁rates或charges对象将调用基类析构函数。这里也不需要多态性。

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

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