34

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

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

  • 它通过一个指针(指向虚拟方法表)减少实例的大小,并且
  • 它使破坏和建造速度更快。

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

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

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

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

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

编辑2:

正如@doc 指出的那样,C++ 11更高级的using声明在一定程度上降低了组合成本。

4

9 回答 9

29

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

于 2009-10-30T00:15:34.490 回答
18

我认为 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 概念。它们只是最佳解释中的抽象数据类型

于 2009-10-30T00:33:09.943 回答
14

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

于 2009-10-30T00:15:51.727 回答
8

为什么 STL 容器没有设计为允许继承?

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

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

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


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

为什么不使用protected或继承并使用关键字private公开所需的接口部分?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<int>并且他是安全的,因为非虚拟析构函数的唯一问题是当对象作为父类的实例被删除时它不会调用派生的析构函数。

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

于 2012-12-14T15:00:22.627 回答
1

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

于 2009-10-30T20:08:21.730 回答
1

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

高级 C++:虚拟析构函数和智能析构函数

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;
}

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

于 2015-11-23T22:58:05.570 回答
0

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

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

于 2009-10-30T10:40:08.367 回答
-2

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

于 2009-10-30T00:15:39.437 回答
-2

如果你真的需要虚拟析构函数,你可以将它添加到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
于 2016-01-13T17:33:38.313 回答