计数体技术
Kevlin Henney
([email protected])
引用计数技术?你可能会想,这没什么新鲜的。每一本好的 C++ 教材,都会在中级或高级阶段介绍这个概念。过去,它已经被如此彻底地探讨过,以至于你可能会认为所有能说的都已经说过了。好吧,让我们从基本原理开始,看看我们是否能发掘出一些新的东西……
然后就什么也没有了……
引用计数背后的原理是保持对对象的使用计数,以便当计数降为零时,我们知道该对象未被使用。这通常用于简化动态分配对象的内存管理:保持对该对象持有的引用数量的计数,并在计数为零时删除该对象。
如何跟踪对象的用户数量?普通指针非常简单,因此需要额外的间接层来管理计数。这本质上是《设计模式》[Gamma, Helm, Johnson & Vlissides, Addison-Wesley, ISBN 0-201-63361-2] 中描述的代理模式。其意图如下:
为另一个对象提供一个代理或占位符,以控制对它的访问。
Coplien [《高级 C++ 编程风格与惯用法》,Addison-Wesley,ISBN 0-201-56365-7] 定义了一组与句柄和主体部分的基本分离相关的惯用法。《Taligent 程序设计指南》[Addison-Wesley,ISBN 0-201-40888-0] 为代理(也称为代理)确定了许多特定类别。广义上讲,它们分为两大类
- 隐藏:句柄是感兴趣的对象,隐藏了主体本身。句柄的功能是通过委托给主体来获得的,句柄的用户不知道主体。引用计数字符串提供了一种透明的优化。主体在字符串的副本之间共享,直到需要更改为止,此时才会进行复制。这种写时复制模式(延迟求值的特殊形式)需要使用隐藏的引用计数主体。
- 显式:这里主体是感兴趣的,句柄只是为其访问和管理提供智能。在 C++ 中,这通常实现为智能指针惯用法。其中一个应用是引用计数智能指针,它们协作以保持对象的计数,并在计数降为零时将其删除。
附加 vs 分离
对于引用计数智能指针,计数可以存在于两个地方,从而产生两种不同的模式,这两种模式都在《软件模式》[Coplien, SIGS, ISBN 0-884842-50-X] 中有所概述
- 计数体或附加计数句柄/体 将计数放在被计数的对象内。好处是可计数性是被计数对象的一部分,并且引用计数不需要额外的对象。缺点很明显,这是侵入式的,并且当对象不是基于堆时,引用计数的空间会被浪费。因此,引用计数将您绑定到特定的实现和使用方式。
- 分离计数句柄/体 将计数放在被计数的对象之外,以便它们一起处理。这样做的明显好处是这种技术是完全非侵入式的,所有的智能和支持装置都在智能指针中,因此可以用于独立于引用计数指针创建的类。主要缺点是频繁使用会导致在堆上创建大量的小对象,即计数器。
即使通过这种简单的分析,分离计数句柄/体方法似乎也更胜一筹。事实上,随着模板的日益使用,这通常是首选方法,并且是常见的(但非标准的)`counted_ptr` 背后的原理。 *[Boost 的名称是 `shared_ptr` 而不是 `counted_ptr`。]*
计数体的一种常见实现是在计数类型派生自的基类中提供计数机制。要么如此,要么为每个需要它的类重新提供引用计数机制。这两种方法都不令人满意,因为它们非常封闭,将类耦合到特定的框架中。除此之外,在非计数对象中存在休眠计数的非内聚性,你会感觉,除了在 COM 和 CORBA 等广泛使用的对象模型中使用之外,计数体方法可能只在特定情况下有用。
基于需求的方法
正是开放性的问题让我重新思考计数体惯用法的问题。是的,使用这种惯用法时,预期会有一定程度的侵入性,但是否有什么方法可以将其最小化,并将计数机制的选择与所使用的智能指针类型解耦?
近年来,用于构建开放式通用组件的最具指导意义的代码和规范是 Stepanov 和 Lee 的 STL(标准模板库),现在是 C++ 标准库的一部分。STL 方法广泛使用基于类型明确定义的操作需求的编译时多态性。例如,每个容器、包含和迭代器类型都由应该对该类型的对象执行的操作来定义,通常带有描述附加约束的注释。顾名思义,编译时多态性基于函数名称和参数用法(即重载)在编译时解析函数。与基于类型、名称和函数签名的运行时多态性相比,这种方法的侵入性更小,尽管如果出错则更难诊断。
这种基于需求的方法可以应用于引用计数。我们需要一个类型为“可计数”的操作大致如下
- 一个 `acquire` 操作,注册对可计数对象的兴趣。
- 一个 `release` 操作,取消注册对可计数对象的兴趣。
- 一个 `acquired` 查询,返回一个可计数对象当前是否被获取。
- 一个 `dispose` 操作,负责处理不再被获取的对象。
请注意,计数被推断为此类型抽象状态的一部分,并且不以任何其他方式提及或定义。这种方法的开放性部分源于全局函数的使用,这意味着不暗示任何特定的成员函数;这是在不修改类本身的情况下包装现有计数体类的完美方法。开放性的另一个方面来自对操作的更精确的规范。
对于一个类型为“可计数”的类型,它必须满足以下要求,其中 `ptr` 是指向该类型单个对象(即不是数组)的非空指针,*`#function`* 表示对 `*function(ptr)*` 的调用次数
表达式 | 返回类型 | 语义和注释 |
acquire(ptr) |
无要求 | 后置条件:`acquired(ptr)` |
release(ptr) |
无要求 | 前置条件:`acquired(ptr) ` 后置条件:`acquired(ptr) == #acquire - #release` |
acquired(ptr) |
可转换为 `bool` | 返回值:`#acquire > #release` |
dispose(ptr, ptr) |
无要求 | 前置条件:`!acquired(ptr) ` 后置条件:`*ptr` 不再可用 |
请注意,`dispose` 的两个参数是为了支持选择要调用的函数的适当类型安全版本。在一般情况下,意图是第一个参数确定要删除的类型,通常是模板化的,而第二个参数选择使用哪个模板,例如通过符合特定的基类。
此外,还必须满足以下要求,其中 `null` 是指向可计数类型的空指针
表达式 | 返回类型 | 语义和注释 |
acquire(null) |
无要求 | 操作:无 |
release(null) |
无要求 | 操作:无 |
acquired(null) |
可转换为 `bool` | 返回值:`false` |
dispose(null, null) |
无要求 | 操作:无 |
请注意,除了这些函数本身应该是异常安全的之外,对这些函数没有关于抛出或不抛出异常的要求。
变得智能
给定类型的可计数要求,可以定义一个使用它们进行引用计数的通用智能指针类型
template<typename countable_type> class countable_ptr { public: // construction and destruction explicit countable_ptr(countable_type *); countable_ptr(const countable_ptr &); ~countable_ptr(); public: // access countable_type *operator->() const; countable_type &operator*() const; countable_type *get() const; public: // modification countable_ptr &clear(); countable_ptr &assign(countable_type *); countable_ptr &assign(const countable_ptr &); countable_ptr &operator=(const countable_ptr &); private: // representation countable_type *body; };
为了便于说明,该类的接口被有意地保持简单,例如省略了成员模板和 `throw` 规范。大多数函数的实现都非常简单,非常依赖于作为关键函数的 `assign` 成员
template<typename countable_type> countable_ptr<countable_type>::countable_ptr(countable_type *initial) : body(initial) { acquire(body); } template<typename countable_type> countable_ptr<countable_type>::countable_ptr(const countable_ptr &other) : body(other.body) { acquire(body); } template<typename countable_type> countable_ptr<countable_type>::~countable_ptr() { clear(); } template<typename countable_type> countable_type *countable_ptr<countable_type>::operator->() const { return body; } template<typename countable_type> countable_type &countable_ptr<countable_type>::operator*() const { return *body; } template<typename countable_type> countable_type *countable_ptr<countable_type>::get() const { return body; } template<typename countable_type> countable_ptr<countable_type> &countable_ptr<countable_type>::clear() { return assign(0); } template<typename countable_type> countable_ptr<countable_type> &countable_ptr<countable_type>::assign(countable_type *rhs) { // set to rhs (uses Copy Before Release idiom which is self assignment safe) acquire(rhs); countable_type *old_body = body; body = rhs; // tidy up release(old_body); if(!acquired(old_body)) { dispose(old_body, old_body); } return *this; } template<typename countable_type> countable_ptr<countable_type> &countable_ptr<countable_type>::assign(const countable_ptr &rhs) { return assign(rhs.body); } template<typename countable_type> countable_ptr<countable_type> &countable_ptr<countable_type>::operator=(const countable_ptr &rhs) { return assign(rhs); }
公共责任制
符合要求意味着一个类型可以与 `countable_ptr` 一起使用。这里有一个实现混入类(*mix-imp*),它通过成员函数赋予其派生类可计数性。这个类可以用作类适配器
class countability { public: // manipulation void acquire() const; void release() const; size_t acquired() const; protected: // construction and destruction countability(); ~countability(); private: // representation mutable size_t count; private: // prevention countability(const countability &); countability &operator=(const countability &); };
请注意,操作函数是 `const`,而 `count` 成员本身是 `mutable`。这是因为可计数性不是对象抽象状态的一部分:内存管理不依赖于对象的 `const` 性。这里我不包含成员函数的定义,因为你可能可以猜到它们:分别用于操作函数的递增、递减和返回当前计数。在多线程环境中,您应该确保此类读写操作是原子的。
那么我们如何使这个类可计数呢?一组简单的转发函数就可以完成这项工作
void acquire(const countability *ptr) { if(ptr) { ptr->acquire(); } } void release(const countability *ptr) { if(ptr) { ptr->release(); } } size_t acquired(const countability *ptr) { return ptr ? ptr->acquired() : 0; } template<class countability_derived> void dispose(const countability_derived *ptr, const countability *) { delete ptr; }
现在任何派生自 `countability` 的类型都可以与 `countable_ptr` 一起使用
class example : public countability { ... }; void simple() { countable_ptr<example> ptr(new example); countable_ptr<example> qtr(ptr); ptr.clear(); // set ptr to point to null } // allocated object deleted when qtr destructs
运行时混入
挑战在于以非侵入式的方式应用计数体,这样当对象未被计数时就不会有开销。我们想要做的是在每个对象而不是每个类的基础上赋予这种能力。实际上,我们追求的是任何对象的*可计数性*,即任何 `void *` 指向的东西!不用说,`void` 可能是所有类型中最不确定的类型。
解决这个问题的力量非常有趣,至少可以说。有趣,但并非不可克服。鉴于运行时对象的类不能以任何明确定义的方式动态更改,并且对象的布局必须是固定的,我们必须找到一个新的位置和时间来添加计数状态。事实上,这必须只在堆创建时添加,这表明了以下解决方案
struct countable_new; extern const countable_new countable; void *operator new(size_t, const countable_new &); void operator delete(void *, const countable_new &);
我们重载了带有虚拟参数的operator new
,以将其与常规的全局operator new
区分开来。这类似于标准库中std::nothrow_t
类型和std::nothrow
对象的使用。placement operator delete
用于在构造失败时执行清理工作。需要注意的是,目前并非所有编译器都支持此功能。
使用countable
的new
表达式的结果是在堆上分配一个对象,该对象具有一个保存计数的头块,即我们通过添加前缀扩展了该对象。我们可以在实现文件中提供一些匿名命名空间(未显示)中的功能,以支持计数及其从原始指针的访问。
struct count { size_t value; }; count *header(const void *ptr) { return const_cast<count *>(static_cast<const count *>(ptr) - 1); }
这里需要注意的一个重要约束是count
的对齐方式应使其适合任何类型的对齐要求。对于所示的定义,几乎所有平台都满足此条件。但是,您可能需要为不满足条件的平台添加填充成员,例如使用匿名union
来共同对齐count
和对齐要求最高的类型。遗憾的是,没有可移植的方法来指定此要求,以便同时遵守最小对齐要求 - 这是指定不直接使用new
或malloc
结果的自定义分配器时遇到的常见问题。
再次强调,计数不被视为对象逻辑状态的一部分,因此从const
到非const
的转换 - 实际上count
是一个mutable
类型。
分配器函数本身相当简单。
void *operator new(size_t size, const countable_new &) { count *allocated = static_cast<count *>(::operator new(sizeof(count) + size)); *allocated = count(); // initialise the header return allocated + 1; // adjust result to point to the body } void operator delete(void *ptr, const countable_new &) { ::operator delete(header(ptr)); }
给定一个正确分配的标头,我们现在需要在const void *
上操作的 *Countable* 函数来完成整个过程。
void acquire(const void *ptr) { if(ptr) { ++header(ptr)->value; } } void release(const void *ptr) { if(ptr) { --header(ptr)->value; } } size_t acquired(const void *ptr) { return ptr ? header(ptr)->value : 0; } template<typename countable_type> void dispose(const countable_type *ptr, const void *) { ptr->~countable_type(); operator delete(const_cast<countable_type *>(ptr), countable); }
其中最复杂的是dispose
函数,它必须确保正确地析构类型,并且还要从正确的偏移量回收内存。它使用第一个参数的值和类型来正确执行此操作,第二个参数仅充当策略选择器,即使用const void *
将其与先前为const countability *
显示的dispose区分开来。
变得更智能
现在我们有了一种在创建时为任何类型的对象添加可计数性的方法,那么还需要哪些额外的工作才能使它与我们之前定义的countable_ptr
一起工作呢?好消息:什么都不需要!
class example { ... }; void simple() { countable_ptr<example> ptr(new(countable) example); countable_ptr<example> qtr(ptr); ptr.clear(); // set ptr to point to null } // allocated object deleted when qtr destructs
new(countable)
表达式定义了不同的分配和释放策略,与其他分配器一样,任何混合分配策略的尝试,例如在使用new(countable)
分配的对象上调用delete
,都会导致未定义的行为。这类似于将new[]
与delete
混合使用或将malloc
与delete
混合使用时发生的情况。*Countable*一致性的重点是*Countable*对象与countable_ptr
一起使用,这确保了正确使用。
然而,意外总会发生,您可能不可避免地忘记使用new(countable)
进行分配,而是使用了new
。在大多数情况下,可以通过扩展此处显示的代码来向count
添加一个检查成员,并在每次访问时验证该检查,从而检测到此错误和其他错误。确保头文件和实现源文件之间清晰分离的好处意味着您可以引入此分配器的检查版本,而无需重新编译代码。
结论
本文介绍了两个关键概念
- 使用基于通用需求的方法来简化和调整计数体(COUNTED BODY)模式的使用。
- 能够通过控制分配,使用运行时混合(RUNTIME MIXIN)模式动态且非侵入地向固定类型添加功能。
将两者结合应用,产生了基本计数体模式的新变体,即非侵入式计数体(UNINTRUSIVE COUNTED BODY)。您可以进一步扩展此主题,并为 C++ 设计一个简单的垃圾收集系统。
countable_ptr
、countability
和countable new
的完整代码也可用。