C++: Inheriting from std::map

C++: Inheriting from std::map

本文关键字:map std Inheriting C++ from      更新时间:2023-10-16

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

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

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

如果尝试通过指向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);
}

有一种误解:继承——在纯 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++强制性的结果,而只是一个普遍接受的未编写的规则(规范中没有任何内容:除了 UB 在基础上调用删除(规则出现在 C++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); delete p; ,尽管第二种情况与第一种情况完美结合,看起来没有任何明显原因的人都很有趣。

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

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

但是您的问题有所不同:

调用 B::~

B(((通过删除或作用域结尾(将始终导致 A::~A((,因为 A(无论是嵌入的还是继承的(在任何情况下都是 B 的一部分


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

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

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

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

std::map 不是多态的,所以它是不可替换的。MyMap 是一样的:不是多态的,也不是不可替换的。

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

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

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

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

为什么?

继承有两个传统原因:

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

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


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

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

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

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

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

@Matthieu M 你说

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

为什么?

继承有两个传统原因:

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

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

关于"前者":

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

关于"后者":

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

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

我还使用受保护的typedef std::map<key,valueClass*> baseClassMap;作为派生类映射声明的前 2 行,以便在迭代器循环删除派生映射中包含的所有valueClass*实例后,我可以在重写的 clear() 函数中调用baseClassMap::clear();,这使得维护更容易valueClass*类型发生变化。

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

ISO CPP 核心准则 C.120 和 C.129 清楚地表明,继承适用于抽象接口或基类,旨在由派生类补充。前者在层次结构的根部具有纯虚函数,后者通常具有派生类使用的非虚拟但受保护的函数。

国际标准化组织 CPP 指南

我同意用于其他用途的继承可能看起来很方便。然而,C++的设计者的方向是,代码维护的成本和将继承误用于其他目的的代码设计缺点,超过了任何便利的好处。

帮助程序函数或组合的另一种替代方法是使用 map 本身使用的一些较低级别的部分并创建新的映射类。

相关文章: