我什么时候可以安全地从C++中的基类继承

When can I safely inherit from a base class in C++

本文关键字:C++ 基类 继承 什么时候 安全      更新时间:2023-10-16

所以我在任何地方都能找到的基本规则是,要从基类继承,基类必须有一个虚拟析构函数,这样才能实现以下功能:

Base *base = new Inherited();
delete base;

然而,我确信我至少看到了另一种允许安全继承的可能性。然而,我在任何地方都找不到它,我觉得我要疯了。我认为另一种选择可能是基类有一个琐碎的析构函数,但根据非虚拟琐碎析构函数+继承,事实并非如此。尽管在这种情况下不会发生内存泄漏,但这似乎仍然是未定义的行为。

其他人知道另一种情况是什么吗?或者你能肯定地告诉我是我梦见的吗?

我想一个例子可以是涉及shared_ptr的例子,因为它很好地展示了问题的两面。

假设您有一个类B,它有一个平凡的非虚拟析构函数,而派生类D有自己的复杂析构函数。

让我们在某处定义以下函数:

shared_ptr<B> factory () {
    // some complex rules at the very end of which you decide to instantiate class D
    return make_shared<D>();
}

在这种情况下,由于多态性,您正在处理所有有趣的特性,但您正在处理的指针从用类型D构造的指针继承了deleter

尽管由于类型擦除,类型被隐藏在某个地方,一切都很好,但实际调用的析构函数是D的析构构函数,因此从这个角度来看,一切都应该很好,即使B的析构因子不是virtual

相反,如果您将上述工厂定义为:

B* factory () {
    return new D{};
}

所谓的析构函数(好吧,假设有人会删除它)将是B中的一个,这不是你想要的。

也就是说,将要继承的类的析构函数定义为virtual是一种很好的做法,否则将final放在类定义中,并停止层次结构。

还有很多其他的例子,这不是工作的唯一情况,但它可以帮助解释为什么它工作。

可能是在继承是私有的情况下。在这种情况下,用户无法将Derived*转换为Base*,因此无法尝试通过基类指针删除派生类。当然,您仍然需要注意,在Derived的实现中,您不会在任何地方这样做。

我对此的看法是务实的,而不是与标准允许或不允许的内容有关。

因此,务实地,如果一个类没有虚拟析构函数,即使是空的,那么我的默认假设是它没有被设计成基类。这可能比破坏有更多的影响,在更多的情况下,这只是打开了一个蠕虫罐头,让你以后陷入其中。

如果您希望或需要使用没有虚拟析构函数的类中的功能,那么使用组合而不是继承会更安全。事实上,这是首选路线。

我提到的另一种情况是使基类析构函数protected。这样,就可以防止通过基类进行删除。

这实际上是Herb Sutter等人的《C++编码标准》一书中的第50项:"使基类析构函数公共且虚拟,或受保护且非虚拟",所以你很可能以前听说过它。

您总是可以从类继承。不过,也有一些规则需要遵守,例如,如果没有虚拟析构函数,就不能以多态方式调用析构函数。为了避免这种情况,例如,您可以对不打算作为基类的基类使用私有派生,例如STL中的容器。

正如其他人所提到的,只要你通过它自己的析构函数删除类,换句话说,你就完成了

Inherited *ip = new Inherited();
Base *p = ip; 
... 
delete ip;

你会没事的。有几种不同的方法可以做到这一点,但你必须非常小心,以确保情况确实如此。

但是,基类中有一个空的析构函数[并且您继承的类型是立即继承的],只有当它为TRULY空时才有效,而不仅仅是析构函数体有一个{ }。[参见下面的第2版!]

例如,如果基类中有一个vectorstd::string,或者任何其他需要销毁的类,那么就会泄露该类的内容。换句话说,您需要100%确保基类的析构函数为空。我不知道有什么编程方法可以确定这一点(除了分析生成的代码之外)。

编辑:

还要注意"未来的变化"——例如,在Base中添加字符串或向量,或者将基类从Base更改为具有"带内容"析构函数的SomethingInheritedFromBase,都会破坏"空析构函数"概念。

第2版:

需要注意的是,对于"析构函数为空",在所有派生类中也必须有真正的空析构函数。有些类没有需要销毁的成员(例如,接口类通常没有数据成员,因此本身不需要销毁),因此可以构造这样的情况,但我们必须非常小心,避免派生类的析构函数向类中添加析构函数。