不可继承类






4.08/5 (29投票s)
一个不能被继承的类
引言
我们为什么不应该从 STL 类继承,这是一个常见的问题。这个问题的答案很简单。三大规则 [CLI95]。换句话说,如果你想让一个类成为基类,那么它应该有一个虚析构函数。然而,STL 中没有一个类定义了虚析构函数。即使你尝试从 STL 类继承,编译器也不会报错,但程序中存在资源泄露的高风险。如果编译器能在编译期间捕获尽可能多的错误,就能让程序员的生活更轻松。因此,将一个类设计成无法被继承(即,一个 Final 类)是一个好主意。
背景
有一天,我和一位主要从事 Java 工作的朋友讨论时,比较了 Java 和 C++ 的面向对象特性。他向我介绍了一个 Java 中称为 Final 类的有趣概念,即一个类不能再被进一步继承。当时对我来说这是一个全新的概念,我非常喜欢它,因为我经常希望能在 C++ 中创建一个别人无法继承的类。他让我用 C++ 实现这一点,于是我开始思考并进行了一些实验。在进一步讨论如何实现之前,讨论一下我们为什么需要它是有用的。
我们为什么可能需要 Final 类?
一个可能的场景,我想到的“Final 类”可能有用的是一个没有虚析构函数的类。假设你创建了一个没有虚析构函数的类,它可能包含一些动态分配的对象。现在,如果有人从中继承一个类,并动态创建派生类对象,那么就会明显地产生资源泄露,尽管编译器不会对此提出警告。
class B { . . . }; class D : public B { . . . }; B* pB = new D; delete pB; // resource leak
实现 Final 类的细节
为了使一个类成为 final,一个可能想到的第一个解决方案是将其构造函数设为私有。然后使用静态函数来创建对象。但这种方法有一个问题,即类中可能有多个构造函数,你必须将它们全部设为私有。仍然有可能有人创建另一个公共构造函数。解决这个问题的快速方法是将析构函数设为私有而不是构造函数,因为类中只能有一个析构函数。
class FinalClass { private: ~FinalClass() { } public: static FinalClass* CreateInstance() { return new FinalClass; } . . . };这种方法的一个问题是对象不能在栈上创建;对象必须在堆上创建。现在,由该类的用户负责销毁该对象。在这种情况下,如果用户忘记在不再需要对象时从堆中删除该类的对象,则存在资源泄露的风险。
让我们来看看另一种实现 Final 类的方法,同时仍然允许该类的客户端在栈上创建对象,而不仅仅是在堆上。我们都知道,当我们使一个类成为另一个类的友元时,它可以创建该类的对象,即使其析构函数是私有的。因此,我们可以创建一个临时类并将其构造函数设为私有。然后继承一个类,并使派生类成为其基类的友元,因为如果我们不这样做,我们就无法继承一个构造函数或析构函数为私有的类的类。
class Temp { private: ~Temp() { }; friend class FinalClass; }; class FinalClass : public Temp { . . . };但是当你从
FinalClass
继承任何类时,这都可以正常工作。假设你从 FinalClass
派生了一个派生类。现在当你创建派生类对象时,它的构造函数会被调用,它会调用 FinalClass
的构造函数,而 FinalClass
的构造函数会调用 Temp
的构造函数,因为 FinalClass
是 Temp
的友元。所以一切都很正常,我们的 Final 类仍然可以被继承,换句话说,它目前不是 final 的。在更详细地讨论 Final 类之前,让我们讨论著名的多重继承问题,或者说是菱形问题。在这种情况下,你通过将基类设为虚基类并虚继承中间类来解决该问题。但是虚基类的目的是什么?事实上,大多数派生类(创建了其对象的类)的对象的构造函数会直接调用虚基类的构造函数。所以我们解决了菱形问题,因为现在最派生的类(在菱形问题中是底部)直接调用最基类(在菱形问题中是顶部)的构造函数,现在顶部类只有一个副本。
我们可以在这里应用相同的技术。我们让 FinalClass
虚继承自 Temp
类,并将 Temp
设为虚基类。现在,每当有人尝试从 FinalClass
继承类并创建其对象时,其构造函数会尝试调用 Temp
的构造函数。但 Temp
的构造函数是私有的,所以编译器会报错,并在编译期间给出错误,因为你的派生类不是 Temp
的友元。请记住,友元关系不会在派生类中继承。毕竟,你父母的朋友不是你的朋友,你的朋友也不是你孩子的朋。但当你创建 FinalClass
对象时,它的构造函数可以调用 Temp
的构造函数,因为 FinalClass
是 Temp
的友元。所以这是我们 Final 类的最终版本,它也可以在栈上创建,而不仅仅是在堆上。
class Temp { private: ~Temp() { }; friend class FinalClass; }; class FinalClass : virtual public Temp { . . . };但是,如果你需要创建多个 Final 类,情况又如何呢?你必须编写两倍的类,即,如果你需要 N 个 Final 类,那么你必须编写 2N 个类,因为在这个方法中,我们有一个带有私有构造函数的临时类和你的 Final 类。所以你必须一遍又一遍地编写大量相同的代码。为什么不尝试利用模板,并尝试将一个类设计成这样,如果你从该类继承任何类,你的类将自动成为 Final 类。这种类的代码非常简单。
template <typename T> class MakeFinal { private: ~MakeFinal() { }; friend T; };不要忘记虚继承你的 Final 类,以确保它成为虚基类。并将你的 Final 类名作为模板参数传递,这样你的类就成了它的友元,并可以调用它的构造函数。
class FinalClass : virtual public MakeFinal<FinalClass> { };
结论
当然,任何事物都不是没有代价的。你必须支付额外的字节来存储关于虚基类的信息。大多数编译器实现使用一个指针来存储虚继承情况下虚基类的信息。因此,对象的大小不仅仅是所有成员变量存储分配的总和,而比你预期的要大。请记住,这个指针,即指向虚基类的指针,与虚函数情况下引入的虚指针不同 [LIP96]。这完全是一个实现问题;C++ 标准没有关于调用虚函数、虚指针和虚表机制的规定 [ISO98]。
只添加一个小类并更改几行代码,就可以使你的程序更可靠,并减少如果你不使用 Final 类而可能产生的资源泄露问题。
参考
- [CLI95] 1995 C++ FAQs Marshall P Cline, Greg A Lomow
- [ISO98] 1998 International Standard Programming Language C++
- [LIP96] 1996 Inside the C++ Object Model Stanley B Lippman