26

我想继承自std::map,但据我所知std::map没有任何虚拟析构函数。

因此,是否可以std::map在我的析构函数中显式调用 ' 析构函数以确保正确销毁对象?

4

4 回答 4

35

析构函数确实被调用,即使它不是虚拟的,但这不是问题。

如果您尝试通过指向std::map.

使用组合而不是继承,std容器不应该被继承,你不应该。

我假设您想扩展std::map(假设您想找到最小值)的功能,在这种情况下,您有两个更好且合法的选项:

1)如建议的那样,您可以改用组合:

template<class K, class V>
class MyMap
{
    std::map<K,V> m;
    //wrapper methods
    V getMin();
};

2) 自由功能:

namespace MapFunctionality
{
    template<class K, class V>
    V getMin(const std::map<K,V> m);
}
于 2012-05-07T06:48:26.573 回答
21

有一个误解:继承——在纯 OOP 的概念之外,C++ 不是——只不过是“具有未命名成员的组合,具有衰减能力”。

没有虚函数(从这个意义上讲,析构函数并不特殊)使您的对象不是多态的,但是如果您所做的只是“重用它的行为并公开本机接口”,那么继承正是您所要求的。

析构函数不需要相互显式调用,因为它们的调用总是按规范链接。

#include <iostream>
unsing namespace std;

class A
{
public:
   A() { cout << "A::A()" << endl; }
   ~A() { cout << "A::~A()" << endl; }
   void hello() { cout << "A::hello()" << endl; }
};

class B: public A
{
public:
   B() { cout << "B::B()" << endl; }
   ~B() { cout << "B::~B()" << endl; }
   void hello() { cout << "B::hello()" << endl; }
};

int main()
{
   B b;
   b.hello();
   return 0;
}

将输出

A::A()
B::B()
B::hello()
B::~B()
A::~A()

使 A 嵌入到 B 中

class B
{
public:
   A a;
   B() { cout << "B::B()" << endl; }
   ~B() { cout << "B::~B()" << endl; }
   void hello() { cout << "B::hello()" << endl; }
};

这将输出完全相同。

“如果析构函数不是虚拟的,则不要派生”不是 C++ 的强制结果,而只是在 C 之前出现的一个普遍接受的未编写的规则(规范中没有关于它的内容:除了一个 UB 在基础上调用 delete)规则++99,当动态继承和虚函数的 OOP 是 C++ 唯一支持的编程范式时。

当然,世界各地的许多程序员都是在那种学校里建立起来的(就像把 iostreams 作为原语教授,然后转到数组和指针一样,在最后一课老师说“哦……tehre 也是具有向量、字符串等高级特性的 STL”),到了今天,即使 C++ 变成了多范式,仍然坚持这个纯粹的 OOP 规则。

在我的示例中,A::~A() 与 A::hello 并不完全一样。这是什么意思?

简单:出于同样的原因,调用A::hello不会导致调用B::hello,调用A::~A()(通过删除)不会导致B::~B(). 如果您可以接受- 在您的编程风格中 -第一个断言,那么您没有理由不能接受第二个。在我的示例中A* p = new B,不会收到,delete p因为 A::~A 不是虚拟的,我知道这意味着什么

完全相同的原因,使用 B 的第二个示例,A* p = &((new B)->a);带有 a delete p;,尽管第二个案例与第一个案例完全对偶,但没有明显的原因,看起来没有任何人感兴趣。

唯一的问题是“维护”,从某种意义上说——如果 yopur 代码被 OOP 程序员查看——会拒绝它,不是因为它本身是错误的,而是因为他被告知要这样做。

事实上,“如果析构函数不是虚拟的,就不要派生”是因为大多数程序员认为有太多程序员不知道他们不能在指向 base 的指针上调用 delete。(对不起,如果这不是礼貌,但经过 30 多年的编程经验,我看不出任何其他原因!)

但你的问题不同:

调用 B::~B() (通过删除或范围结束)将始终导致 A::~A() 因为 A (无论它是嵌入的还是继承的)在任何情况下都是 B 的一部分


在 Luchian 评论之后:在他的评​​论中提到的未定义行为与在没有虚拟析构函数的指向对象的基础上的删除有关。

根据 OOP 学校的说法,这导致了“如果不存在虚拟析构函数,则不要派生”的规则。

我在这里要指出的是,该学派的原因取决于这样一个事实,即每个面向 OOP 的对象都必须是多态的,并且所有都是多态的都必须可以通过指向基的指针来寻址,以允许对象替换。通过做出这些断言,该学校故意试图使派生和不可替代之间的交集无效,以便纯粹的 OOP 程序不会体验到 UB。

简单地说,我的立场是承认 C++ 不仅仅是 OOP,而且并非所有 C++ 对象都必须默认面向 OOP,并且承认 OOP 并不总是必要的需求,也承认 C++ 继承不一定总是服务于 OOP替代。

std::map 不是多态的,因此不可替换。MyMap 是一样的:不是多态的,也不是可替换的。

它只需要重用 std::map 并公开相同的 std::map 接口。继承只是避免重写函数的长样板的方法,这些函数只是调用重用的函数。

MyMap 将没有虚拟 dtor,因为 std::map 没有虚拟 dtor。这 - 对我来说 - 足以告诉 C++ 程序员这些不是多态对象,不能用一个来代替另一个。

我不得不承认,今天大多数 C++ 专家并不认同这个立场。但我认为(我唯一的个人意见)这只是因为他们的历史,这与 OOP 作为一种服务的教条有关,而不是因为 C++ 的需要。对我来说,C++ 不是纯粹的 OOP 语言,在不遵循或不需要 OOP 的情况下,不一定总是遵循 OOP 范式。

于 2012-05-07T08:21:04.890 回答
13

我想继承std::map[...]

为什么 ?

继承有两个传统原因:

  • 重用其接口(因此,针对它编码的方法)
  • 重用其行为

前者在这里没有任何意义,因为map没有任何virtual方法,所以你不能通过继承来修改它的行为;后者是对继承使用的曲解,最终只会使维护复杂化。


如果没有清楚地了解您的预期用途(您的问题缺乏上下文),我会假设您真正想要的是提供一个类似地图的容器,并带有一些额外的操作。有两种方法可以实现这一点:

  • 组合:您创建一个新对象,其中包含一个std::map,并提供足够的接口
  • 扩展:创建新的自由函数std::map

后者更简单,但也更开放:原来的界面std::map仍然是开放的;因此不适合限制操作。

前者无疑更重量级,但提供了更多的可能性。

由您决定这两种方法中的哪一种更合适。

于 2012-05-07T07:20:13.470 回答
2

@Matthieu M 你说

我想从 std::map [...] 继承

为什么 ?

继承有两个传统原因:

  1. 重用其接口(因此,针对它编码的方法)
  2. 重用其行为

前者在这里毫无意义,因为 map 没有任何虚拟方法,因此您无法通过继承来修改其行为;后者是对继承使用的曲解,最终只会使维护复杂化。

关于“前者”:

clear()函数是虚拟的,对我来说,在派生类中使用迭代器重写 a 是很有意义的std::map<key,valueClass*>::clear(),该迭代器在调用基类之前删除所有指向值类的实例,clear()以防止意外的内存泄漏,它是我实际使用过的一个技巧。至于为什么有人要使用指向类的指针的映射,那么多态性和不可重新分配的引用意味着不能在 STL 容器中使用。您可能会建议使用 reference_wrapper 或智能指针,例如shared_ptr(C++11 特性)但是当你正在编写一个库时,你希望某些人能够使用 C++98 编译器,除非你要求拥有升压,这也可能是不可取的。而且,如果您确实希望地图对其内容拥有唯一所有权,那么您不希望使用 reference_wrapper 或大多数智能指针实现。

关于“后者”:

如果您想要一个指向自动删除指向内存的指针的映射,那么重用“所有”其他映射行为并覆盖 clear 对我来说很有意义,当然,您还需要覆盖赋值/复制构造函数来克隆复制地图时指向对象,这样就不会双重删除valueClass.

但这只需要极少量的编码即可实现。

我还使用 protectedtypedef std::map<key,valueClass*> baseClassMap;作为派生类映射声明的前 2 行,以便在迭代器循环删除派生映射中包含的所有实例后,我可以baseClassMap::clear();在覆盖的函数中调用,这样可以更轻松地进行维护,以防万一类型不断变化。clear()valueClass*valueClass*

关键是,虽然它在良好的编码实践中的适用性可能有限,但我认为说从 map 下降从来都不是一个好主意是不公平的。但也许你有一个更好的主意,我没有想到如何在不添加大量额外源代码(例如聚合 a std::map)的情况下实现相同的自动内存管理效果。

于 2018-04-17T14:23:11.657 回答