使用接口实现面向对象的原始类型






4.65/5 (10投票s)
2004年11月28日
7分钟阅读

59074

369
面向对象模板库 (OOTL) 简介。描述了 OOTL 如何使用 C++ 中定义接口类型的尖端技术,通过 IObject 接口提供轻量级面向对象原语和运行时多态性。
引言
面向对象模板库 (OOTL) 是一个新的开源库,它为 C++ 原语以及 STL 集合提供了轻量级的面向对象替代方案。本文介绍了使我们能够拥有既轻量级又具有运行时多态性的对象的理论和技术,并介绍了 OOTL 原语。
要求
OOTL 需要 Boost C++ 库版本 1.32.0 才能工作,该库可在 www.boost.org 免费获取。Boost 接口库实际上不属于 Boost,但它已包含在本文的源代码中。
关于源代码
本文随附的源代码包括 Boost 接口库的预 Beta 版本,以及 OOTL 的 0.1 版本。有几个名为 xxx-test.hpp 的文件,其中包含 OOTL 的使用示例。BIL 的最佳使用示例可以在 collections.hpp 文件中找到,这些文件定义了各种 OOTL 集合类实现的众多接口。
背景
C++ 缺乏许多现代编程语言中存在的一个特别令我感兴趣的特性:接口。接口通常被视为抽象基类 (ABC) 的形式化表达。我对接口的另一种观点,我认为更直观,就是一组函数签名。实现接口意味着接口所代表的函数签名集对于任何给定对象都是公开可访问的。这两种观点相似但不完全相同。
ABC 不是接口
如果我们接受接口只是一组函数签名,那么就有一个明显的遗漏。没有显式或隐式规定函数应该是 virtual
。相比之下,抽象基类明确要求每个函数都是 virtual
。
这种观点与流行的信念有些矛盾,许多近期编程语言(如 Java 和 C#)中将接口实现为 ABC 就证明了这一点。
动态分派
虚成员函数具有以下特性:对该成员函数的任何调用都将分派给最派生版本。此特性对于接口函数来说并非理想。通过移除此要求,我们可以移除具有虚函数的对象内部所需的 vtable 指针。
接口与虚表开销
大多数 C++ 程序员都熟悉虚函数开销。这基本上意味着,如果你想要运行时多态性,你的项目中就会嵌入一个虚表。在大多数 32 位平台上,这意味着每个对象都需要额外的 32 位空间,无论你是否实际使用多态性。同时,你还会受到动态分派期间的轻微性能损失。换句话说,为了在 C++ 中(以及任何其他面向对象语言中)拥有面向对象的层次结构,你必须忍受代码中动态分派的困扰。
理论上,如果我们想要运行时多态性时使用接口引用变量,我们可以避免虚表开销。接口引用变量然后可以存储类型信息。这就是 BIL (Boost Interfaces Library) 的用武之地。
使用模板生成动态函数分派表并将静态指针存储在接口指针中的技术,首次在我发表于 2004 年 9 月 C/C++ Users Journal 杂志上题为《C++ 中的接口》的文章中进行了描述。Jonathan Turkanis 使用此技术构建了 BIL,该库以其 Alpha 状态包含在本文的源代码中。
关于 BIL
Boost 接口库是由 Jonathan Turkanis 开发的一个非常强大的宏库,尚未正式进入 Beta 阶段。这意味着其通用功能集尚未完全冻结。它已经非常接近 Beta 阶段,因此他允许我将其包含在 OOTL 版本中。不幸的是,目前没有最新的文档,但通过代码中包含的大量案例示例,不难弄清楚它的用法。(话虽如此,可以在此处找到过时的文档版本,但请记住,此文档不完整且已过时。)
IObject 接口
最有趣的接口是 OOTL 原语实现的接口,它允许我们在不需要在编译器上启用 C++ RTTI 的情况下拥有 RTTI(运行时类型信息)。
抽象地说,IObject
接口看起来像
interace IObject { const char* GetClassName(); int GetClassId(); ObjectIdentity GetObjectId(); int GetObjectSize(); }
不幸的是,这种语法在 C++ 中是不可能的——你可以写信给你最喜欢的 C++ 委员会成员,向他们请求这种语法——但是使用 Boost 接口库宏(称为 IDL,用于接口描述语言),接口表达如下:
BOOST_IDL_BEGIN(IObject) BOOST_IDL_FN0(GetObjectSize, int) BOOST_IDL_FN0(GetClassId, int) BOOST_IDL_FN0(GetClassName, char const*) BOOST_IDL_FN0(GetObjectId, ObjectId) BOOST_IDL_END(IObject)
定义此接口后,我们可以使用类型 IObject
来引用任何提供所需函数签名的对象。例如,考虑以下类:
struct FuBar { int GetObjectSize() { return sizeof(FuBar); } int GetClassID() { return 1; } char const* GetClassName() { return "fubar"; } ObjectId GetObjectId() { return static_cast<ObjectID>(this); } }
请注意,此类的函数都不是虚函数,但它使用 IObject
实现了运行时多态性。OOTL 在 object-test.hpp 文件中提供了以下用于测试目的的函数:
void PrintObjectDetails(IObject o) { printf("class = %s, size = %d, object-id = %p, class-id = %x\n", o.GetClassName(), o.GetObjectSize(), o.GetObjectId(), o.GetClassId()); }
当如下使用时
FuBar baz; PrintObjectDetails(baz);
输出与以下内容类似的信息
class = fubar, size = 1, object-id = 001FDE0, class-id = 1
IObject
类型表现得像一个常规的 C++ 引用,指向它被赋值的值,不同之处在于它可以被重新赋值,并且可以引用任何与它所代表接口的函数签名匹配的对象。
OOTL_DEF_OBJECT 宏
为每个类实现 IObject
非常重复,并且需要仔细管理类 ID 以确保它们的唯一性。显然这没什么乐趣,而且我比较懒——这是任何优秀程序员的重要特点——所以我将 IObject
的实现代码写成了一个宏。
OOTL_DEF_OBJECT
宏接受一个参数,即对象的名称,并且必须出现在类声明的任何公共可见部分中。然后它会生成必要的 IObject
实现代码。这意味着我们可以将 FuBar 示例重写为:
struct FuBar {
OOTL_DEF_OBJECT(FuBar)
}
它与我们详细编写的效果完全相同。类 ID 由编译器生成,并保证在项目中每个 OOTL_DEF_OBJECT
声明中都是唯一的。
OOTL 原语
OOTL 提供以下原始类型替代品:Int
、UInt
、Char
、Bool
、Float
和 Dbl
。所有 OOTL 原始类型都实现了 IObject
,并且它们提供了从其内置对应类型到(但不是反向)的隐式转换。由于可能发生的意外副作用,明确没有到原始类型的隐式转换,相反,它们都提供了一个 ToPrimitive()
函数。如果这不符合程序员的喜好,请务必进行更改。OOTL 原始类型还有一个有趣的特性,它们会自动初始化。同样,这可能符合或不符合程序员的喜好,但进行必要的更改仍然相对简单。
OOTL 原语与朴素 OO 原语的比较
许多人听说 OOTL 原语,并自动认为它们与以前的 OO 原语相同,即又慢又臃肿。在 BIL 之前,定义面向对象原语的唯一已知方法是通过抽象基类。类似于以下内容:
struct AbcObject { virtual int GetObjectSize() = 0; virtual int GetClassID() = 0; virtual char const* GetClassName() = 0; virtual ObjectId GetObjectId() = 0; }; struct NaiveInt : public AbcObject { int GetObjectSize() { return sizeof(NaiveInt); } int GetClassID() { return 1; } char const* GetClassName() { return "NaiveInt"; } ObjectId GetObjectId() { return static_cast<ObjectID>(this); } }
这种方法有两个明显的缺点
- 任何从
AbcObject
派生的对象都包含一个额外的虚表指针,并且 - 所有多态性都必须通过预先声明你所继承的对象来预见。
还有性能方面的担忧,我将留给读者自己探索(提示:查看文件 dispatch-timings-test.hpp)。
OOTL 原语与 C++ 原语的比较
与内置原语相比,面向对象原语有一个缺点,它们稍微慢一些。OOTL 原语的优点是它们更容易调试和修改,可以继承和委托,可以多态使用,并且会自动初始化。使用 OOTL 原语的决定当然取决于程序员,但我建议您在下一个项目中仔细考虑它们。您可能会感到惊讶。
结束语
如果对 OOTL 的兴趣足够大,我将撰写一篇关于 OOTL 集合类的后续文章。在那之前,祝您玩 OOTL 愉快,别害羞对其进行大幅修改。如果您用它做出了任何有趣的事情,请告诉我,或者在 Google 上的 OOTL 讨论组中分享您的代码和想法。