【问题标题】:Why don't STL containers have virtual destructors?为什么 STL 容器没有虚拟析构函数?
【发布时间】:2010-12-11 11:07:08
【问题描述】:

有人知道为什么 STL 容器没有虚拟析构函数吗?

据我所知,唯一的好处是:

  • 它将实例的大小减少了一个指针(指向虚拟方法表)和
  • 它使破坏和建造稍微快一点。

缺点是以通常的方式对容器进行子类化是不安全的。

编辑: 也许我的问题可以改写为“为什么不设计 STL 容器以允许继承?”

因为它们不支持继承,所以当人们想要拥有一个需要 STL 功能和少量附加功能的新容器(例如,专门的构造函数或具有默认值的新访问器地图,或其他):

  • 组合和接口复制:创建一个新的模板或类,它拥有作为私有成员的 STL 容器,并且每个 STL 方法都有一个直通内联方法。这与继承一样高效,避免了虚拟方法表的成本(在重要的情况下)。不幸的是,STL 容器具有相当广泛的接口,因此这需要很多行代码才能完成看似容易做到的事情。
  • 只需制作函数:使用裸(可能是模板化的)文件范围函数,而不是尝试添加成员函数。在某些方面,这可能是一种不错的方法,但会失去封装的好处。
  • 具有公共 STL 访问权限的组合:让 STL 容器的所有者允许用户访问 STL 容器本身(可能通过访问器进行保护)。这需要对库编写器进行最少的编码,但对用户来说不太方便。组合的一大卖点是减少了代码中的耦合,但此解决方案将 STL 容器与所有者容器完全耦合(因为所有者返回一个真正的 STL 容器)。
  • 编译时多态性:编写起来可能有些棘手,需要一些代码练习,并不适合所有情况。

作为一个附带问题:是否有一种标准安全的方式来使用非虚拟析构函数进行子类化(假设我不想覆盖任何方法,只是想添加新方法)?我的印象是,如果没有能力更改定义非虚拟类的代码,就没有通用且安全的方法。

编辑 2:

作为@doc points out,C++ 11 更高级的using 声明在一定程度上降低了组合成本。

【问题讨论】:

  • 您错过了“阻止程序员不恰当地使用继承”的好处。从 STL 容器继承可能有正当理由,但我从未找到。
  • 回答你的问题:Prefer Composition to Inheritance.
  • “在某些方面 [非成员函数] 可能是一个好方法,但封装的好处丢失了”。这是错误的。当您仅使用公共接口工作时,完全尊重封装。您可以操作容器(如搜索/排序算法),也可以调整它(如 std::stack)。不要将成员函数语法误认为是抽象的封装,它们是完全独立的东西。
  • @MatthieuM。如果您的类最好被描述为前一个类的子类型,那么您应该继承。如果您正在创建一个名为MyVector 的类,它向标准std::vector<int> 添加了一个方法,那么不要告诉我组合适合这种情况。通过正确使用继承,您可能会收获很多。如果 STL 类没有虚拟 dtors/方法来保持 vtable 远离,那么您可以通过使用继承来获得相同的结果。通过组合,您的类将随着组合成员的大小而增长 - 类似于创建 vtable 的成本。
  • @MatthieuM。恐怕不明白的不是我。您应该参考我的论点,而不是盲目地重复“口头禅”(正如您正确称呼的那样)。并且不要将继承与多态性混淆。如果类没有虚方法,不代表不能继承。

标签: c++ stl destructor


【解决方案1】:

虚拟析构函数只对继承场景有用。 STL 容器并非设计为继承自(也不是受支持的方案)。因此它们没有虚拟析构函数。

【讨论】:

  • 我从面试官那里发现的一件事是,大多数程序员似乎没有理解继承必须是由基类设计的。当人们意识到“它没有虚拟析构函数,因为我不应该继承它”时,看到灯泡亮起来真是太好了。
  • 能否请您说明 STL 容器不是为继承而设计的声明?
  • @JaredPar,你的回答并没有说明为什么
  • 我第二个 @PaulDraper 。提问者想知道为什么这些类没有被设计为继承自。
【解决方案2】:

没有虚拟析构函数会阻止该类正确地成为子类。

【讨论】:

  • 并非如此。您可以对任何没有虚拟析构函数的类进行子类化。您将遇到的唯一问题是在使用多态性的情况下不会调用继承类的析构函数。因此,没有虚拟析构函数不会以任何方式阻止正确的继承。
【解决方案3】:

我猜它遵循 C++ 的理念,即不为您不使用的功能付费。根据平台的不同,如果您不关心虚拟析构函数,那么指向虚拟表的指针可能会付出高昂的代价。

【讨论】:

  • 你是谁?如果我想使用?如果这是最终答案,为什么要提供虚拟表、类和所有这些东西?所有这些都是有代价的。答案仍然很有见地,但不是最后一个......
  • “不为不使用的功能付费”确实是设计 C++ 时的设计目标,不为 STL 容器添加虚拟表从逻辑上遵循此目标。
【解决方案4】:

我认为 Stroustrup 在他精彩的论文中间接回答了这个问题:Why C++ is not just an ObjectOriented Programming Language

7 结束语
是各种 上述设施 面向对象还是不面向对象?哪个? 使用什么定义 面向对象?在大多数情况下,我 认为这些是错误的问题。 重要的是你能有什么想法 表达清楚,你可以多么容易 结合不同的软件 来源,以及效率和 可维护的结果程序 是。换句话说,你如何支持 良好的编程技术和良好的 设计技术比 标签和流行语。根本的 想法只是改进设计和 通过抽象编程。你 想隐藏细节,你想 利用系统中的任何共性, 你想让这个负担得起。 我想鼓励你不要 让面向对象变得毫无意义 学期。 “面向对象”的概念 过于频繁地贬值

– 由 把它等同于好,

– 等同于 使用单一语言,或

– 由 接受一切 面向对象。

我认为 有——而且必须——有用 超越面向对象的技术 编程和设计。然而,为了 避免被完全误解,我 想强调的是,我 不会尝试一个严肃的项目 使用一种编程语言 至少不支持经典 面向对象编程的概念。 除了支持的设施 面向对象编程,我想要—— 和 C++ 提供 - 功能 超出他们支持的人 直接表达概念和 关系。

STL 在构建时主要考虑了三个概念性工具。 通用编程 + 函数式风格 + 数据抽象 == STL 风格。 OOP 不是表示数据结构和算法库的最佳方式,这并不奇怪。尽管在标准库的其他部分中使用了 OOP,但 STL 的设计者发现将上述三种技术混合使用优于 OOP单独。简而言之,该库在设计时并未考虑 OOP,并且在 C++ 中,如果您不使用它,它就不会与您的代码捆绑在一起。你不用为你不使用的东西付费。类 std::vector、std::list、... 是 Java/C# 意义上的不是 OOP 概念。它们只是抽象数据类型的最佳解释。

【讨论】:

  • 冗长且有点离题的答案。
  • 真正的问题是他们为什么需要它们?!他们没有。
  • 我认为这个答案肯定需要编辑以更正 Stroustrup 文章的名称:为什么 C++ 不只是一种面向对象的编程语言。
  • 所以,我会投票给 Stroustrup。我无法相信阻止人们从 STL 类继承的原因是他想要“超越那些支持直接表达概念和关系的特性”。但这个答案仍然很有见地——在我看来,答案确实是一个设计问题。
【解决方案5】:

正如已经指出的那样,STL 容器并非设计为可继承的。没有虚拟方法,所有数据成员都是私有的,没有受保护的 getter/setter/helpers.. 而且正如您所发现的,没有虚拟析构函数..

我建议你真的应该通过组合而不是实现继承来使用容器,以“具有”的方式而不是“是”的方式。

【讨论】:

  • 能否请您说明 STL 容器不是为继承而设计的声明?数据成员几乎总是私有的。没有虚拟方法,因为它们在模板编程中通常很少见。缺少受保护的方法也不是说类不可继承的原因。
【解决方案6】:

你不应该盲目地为每个类添加一个虚拟析构函数。如果是这种情况,该语言将不允许您有任何其他选择。当您将虚拟方法添加到没有任何其他虚拟方法的类时,您只需将类实例的大小增加一个指针的大小,通常为 4 个字节。这取决于您在做什么,这很昂贵。大小增加的原因是创建了一个 v-table 来保存虚拟方法列表,并且每个实例都需要一个指向 v-table 的指针。它通常位于实例的第一个单元格。

【讨论】:

    【解决方案7】:

    为什么 STL 容器的设计不支持继承?

    以我的拙见,他们是。如果他们不这样做,他们就已经最终了。当我查看 stl_vector.h 源代码时,我可以看到我的 STL 实现使用 protected 继承 _Vector_base<_Tp, _Alloc> 来授予派生类的访问权限:

     template<typename _Tp, typename _Alloc = allocator<_Tp> >
     class vector : protected _Vector_base<_Tp, _Alloc>
    

    如果不欢迎子类化,它不会使用 private 继承吗?


    是否有一种标准安全的方式来使用非虚拟析构函数进行子类化(假设我不想覆盖任何方法,只是想添加新方法)? p>

    为什么不使用protectedprivate 继承并使用using 关键字公开所需的接口部分?

    class MyVector : private std::vector<int>
    {
         typedef std::vector<int> Parent;
    
         public:
            using Parent::size;
            using Parent::push_back;
            using Parent::clear;
            //and so on + of course required ctors, dtors and operators.
    };
    

    这种方法可确保类的用户不会将实例向上转换为 std::vector&lt;int&gt; 并且他是安全的,因为非虚拟析构函数的唯一问题是它不会调用派生的析构函数,当对象被删除为父类的一个实例。

    ...我也有一个松散的想法,如果你的类没有析构函数,你甚至可以公开继承。异端?

    【讨论】:

    • 来自 Alexander Stepanov,STL 的创建者stlport.org/resources/StepanovUSA.html “是的。STL 不是面向对象的。我认为面向对象几乎和人工智能一样是骗局。我还没有看到有趣的来自这些 OO 人员的一段代码。”
    • 您不是说:不会将实例向上转换到std::vector&lt;int&gt; 吗?
    • @bloody THX 为普通观众修复了它。但是,我认为如果这些术语具有相反的含义会更好,因为基类是较低级别的接口。
    • 同意,特别是"base"这个词本身的意思就是某物under某物;)
    【解决方案8】:

    另一个能够从 STL 容器继承的解决方案是 Bo Qian 使用智能指针给出的解决方案。

    Advanced C++: Virtual Destructor and Smart Destructor

    class Dog {
    public:
       ~Dog() {cout << "Dog is destroyed"; }
    };
    
    class Yellowdog : public Dog {
    public:
       ~Yellowdog() {cout << "Yellow dog destroyed." << endl; }
    };
    
    
    class DogFactory {
    public:
       static shared_ptr<Dog> createYellowDog() { 
          return shared_ptr<Yellowdog>(new Yellowdog()); 
       }    
    };
    
    int main() {
        shared_ptr<Dog> pd = DogFactory::createYellowDog();
    
        return 0;
    }
    

    这完全避免了虚拟析构函数的困境。

    【讨论】:

    • 工厂模式有什么用?可以简单地做std::shared_ptr&lt;Dog&gt; p = std::make_shared&lt;Yellowdog&gt;();。与虚拟 dtor 相比,不会有任何性能提升,而是相反,因为 shared_ptr 在后台做了很多事情,例如引用计数。
    • 你说得对,不需要工厂模式,但它是强制接口的好方法。我只是复制了薄谦的例子。重点不在于性能增益。使用智能指针只是从 STL 容器子类化的另一种方法,而不必担心虚拟 dtor。
    【解决方案9】:

    如果你真的需要虚拟析构函数,你可以将它添加到vector派生的类中,然后在你需要虚拟接口的任何地方使用这个类作为基类。通过这样做,编译器将从您的基类中调用虚拟析构函数,而基类又会从向量类中调用非虚拟析构函数。

    例子:

    #include <vector>
    #include <iostream>
    
    using namespace std;
    
    class Test
    {
        int val;
    public:
        Test(int val) : val(val)
        {
            cout << "Creating Test " << val << endl;
        }
        Test(const Test& other) : val(other.val)
        {
            cout << "Creating copy of Test " << val << endl;
        }
        ~Test()
        {
            cout << "Destructing Test " << val << endl;
        }
    };
    
    class BaseVector : public vector<Test>
    {
    public:
        BaseVector()
        {
            cout << "Creating BaseVector" << endl;
        }
        virtual ~BaseVector()
        {
            cout << "Destructing BaseVector" << endl;
        }
    };
    
    class FooVector : public BaseVector
    {
    public:
        FooVector()
        {
            cout << "Creating FooVector" << endl;
        }
        virtual ~FooVector()
        {
            cout << "Destructing FooVector" << endl;
        }
    };
    
    int main()
    {
        BaseVector* ptr = new FooVector();
        ptr->push_back(Test(1));
        delete ptr;
    
        return 0;
    }
    

    此代码给出以下输出:

    Creating BaseVector
    Creating FooVector
    Creating Test 1
    Creating copy of Test 1
    Destructing Test 1
    Destructing FooVector
    Destructing BaseVector
    Destructing Test 1
    

    【讨论】:

    • 使用这个解决方案仍然可以删除真正的基类(std::vector)并泄漏。
    猜你喜欢
    • 2015-11-12
    • 2012-04-13
    • 2012-04-18
    • 2011-02-04
    • 2014-02-12
    • 2021-12-09
    • 1970-01-01
    • 2021-07-17
    • 1970-01-01
    相关资源
    最近更新 更多