C++ 中主动对象聚合的技术






2.89/5 (3投票s)
2005年2月3日
9分钟阅读

40585

313
成员变量的分配持久性作为一种提供的行为。
引言
我非常喜欢主动对象模式。在某种程度上,大量的软件可以根据一组组件如何响应系统中的事件来定义。如果假定一个组件的行为是一次只发生一个,那么描述这些组件的行为会更容易理解。主动对象通过确保对象的指定行为是同步的来简化设计规划。它将事件发生的时间与响应异步系统事件的时间解耦。
本文是关于实现主动对象的特定方法的初步描述。我不指望您在没有动机的情况下阅读本文。
主动对象通常需要对其自身的生命周期进行一些控制。它们最常见的职责之一就是简单地等待。比主动对象更简单的组件也会做出类似的承诺。在本文中,我将介绍一种创建类的方法,这些类可以在不与包含它们的类耦合的情况下扩展它们的生命周期。虽然引用计数通常用于那些必须由类的使用者持久化的类的对象,但在这里我们将使用引用计数来允许对象扩展其自身的生命周期或其使用者的生命周期。
包含的源代码用于此技术,源代码基本上是另一种引用计数实现。CodeProject 上已有多篇文章描述了引用计数和智能指针,Peterchen 的《提升代码的智能指针》快速介绍了重要的方面,同时介绍了流行的编程库的实现。
大多数公开可用的引用计数实现都包含某种智能指针类。智能指针的行为类似于指针,但具有额外的智能,可以确保所指向的对象也具有正确的生命周期。智能指针并不总是使用引用计数来确保其分配是可用的(也不应该)。引用计数并不总是意味着使用智能指针(尽管我推荐)。这两个概念是共生的,但可以独立考虑。我包含的源代码包含一个智能指针实现,提供的引用计数实现也可以由包含的智能指针类使用。Boost 的 instrusive_ptr<>
应该是可用的(请注意,此互操作性尚未经过测试)。
我必须承认,仅仅为了查找文章链接给新接触智能指针的读者,我就发现了对我来说是新的材料。 Boost 在智能指针方面有很好的实现和讨论;如果读者对此感兴趣,在那里可能会得到更好的服务。Peterchen 在 CodeProject 文章中对 Boost 智能指针进行了简要描述。
“现代 C++ 编程”中描述的 Loki 库将智能指针概念推进一步,允许将智能指针使用的分配策略应用于内存分配以外的资源。如果您有兴趣,可以阅读更高级的内容。
侵入?多无礼!
在引用计数方法中,一个常见的区别是它们是否具有侵入性。侵入式引用计数方案要求被引用的对象内置支持特定的引用计数实现。非侵入式引用计数则不对被引用类提出此类要求。Boost 和 Loki 都支持侵入式和非侵入式引用计数,但重点偏向于非侵入式。有些人认为侵入式计数有点不雅观,如果不是完全讨厌的话。
非侵入式引用计数可以通过一个代理对象指向目标对象来实现,所有智能指针都跟踪该代理。当智能指针跟踪代理时,它的行为就像指向代理目标的对象。当没有引用代理时,代理将取消引用目标对象和自身。
侵入式意味着对象有一个计数该对象引用的成员变量。COM 组件使用侵入式引用计数。COM 组件将引用操作暴露为虚拟函数,每个组件都根据自己的意愿实现引用行为。C++ 库的另一种典型方法是让被计数对象继承一个支持引用操作的对象,该对象有一个虚拟析构函数,在最后一个引用被释放时调用。后一种方法在对象的生命周期内需要更少的虚拟调用。这样,将意外行为与引用计数操作耦合的诱惑也更小。
非侵入式引用计数是有益的,因为类不会与它们被引用计数的方式耦合。这当然很好,而且在某些情况下,这是划分类与其使用者之间复杂性的合适方式。侵入式引用计数几乎可以提供这种好处。考虑一种要求对象继承自 CCountable
才能进行引用的侵入式引用计数方案。使用引用计数与现有类进行操作是相当可行的
template <class T> class ReferenceCounted : public T, private CReferenceCounting { ReferenceCounted() { } };
如果可行,则在需要 CFoo
时,可以创建 ReferenceCounted< CFoo>
的实例,并具有引用计数行为。我看到的唯一限制是在重载 CFoo
的构造函数时很麻烦。关键是,没有引用可计数对象并不能阻止任何人使用侵入式引用计数。
侵入式引用计数的一个优点是所需分配更少。没有单独的代理分配。根据您的性能要求,这可能重要,也可能不重要。对于优化的异步应用程序,当代码过于复杂无法进一步优化时,堆争用通常是最后一个性能瓶颈。我不想利用程序员过早优化的倾向来推销侵入式引用计数,但堆争用有时是一个重要的考虑因素。
分则必败
大多数侵入式引用计数方案的一个弱点是,对象不能作为其包含类的成员进行聚合。如果一个类也愿意继承引用计数行为,它可以继承一个引用计数对象,但它不能将引用计数对象作为成员变量。如果成员变量引用计数归零,它不知道如何删除它的容器,也不知道何时删除。
一个简单的解决方案是始终使用引用作为成员变量,而不是直接使用被引用类。然而,通过聚合进行的包含与通过引用进行的包含在语义上是不同的,并且这种差异可以表达类生命周期之间关系的特定属性。
使用非侵入式引用计数也可以解决聚合问题。在这种情况下,通常只支持引用分配的类或其某些祖先。虽然可以通过容器引用访问成员,但聚合类在不将引用行为与包含类耦合的情况下无法进行引用计数。这意味着对象无法引用自身,除非它知道它的容器,这限制了该类的重用。
我在这里提出的解决方案是让每个对象的引用行为将其引用计数的变化重定向到其容器的引用行为。引用计数和虚拟析构函数被替换为指向容器的引用计数和虚拟析构函数的指针。我们可以预期对对象的引用操作需要查找容器,但与进行线程安全引用计数修改的现有开销相比,这种开销很小。对象的其余部分仍然直接引用,没有代理。以这种方式支持引用计数,对象可以轻松地在堆上分配,或者作为其他类的成员变量。
指定间接引用行为
通常,引用可计数对象通过继承一个支持引用计数的类来获得这种行为。在下一个代码片段中,CFoo
可以通过继承 ThreadSafeReferenceCountedObject
来进行引用计数。这与包含的代码一起工作,尽管 ThreadSafeReferenceCountedObject
实际上是下面的 typedef
class CFoo : public ThreadSafeReferenceCountedObject { }; typedef ThreadSafeReferenceBehavior< HeapPersistanceBehavior> ThreadSafeReferenceCountedObject;
ThreadSafeReferenceCountedObject
是包含代码中定义的两个策略的规范:引用计数策略(线程安全计数)和持久性策略(在堆上分配)。我们将更改这些策略选择,以使对象可聚合。在包含的代码中,有两种方法可以聚合引用计数对象。它们是
class CFooProxyCount : public ThreadSafeReferenceBehavior< EmbeddedPeristanceBehavior< ThreadSafeReferenceCountedObject > > { CFooProxyCount( ThreadSafeReferenceCountedObject *pAnchor) : ThreadSafeReferenceBehavior< EmbeddedPeristanceBehavior< ThreadSafeReferenceCountedObject > > (pAnchor) { } }; class CFooIndirectCount : public EmbeddedReferenceBehavior< ThreadSafeReferenceCountedObject> { public: CFooIndirectCount(ThreadSafeReferenceCountedObject* pAnchor) : EmbeddedReferenceBehavior< ThreadSafeReferenceCountedObject> (pAnchor) { } };
上述两个类可以与由继承 ThreadSafeReferenceCountedObject
管理的容器进行聚合。CFooProxyCount
维护自己的引用计数,每当代理引用计数非零时,它就会引用容器一次。CFooIndirectCount
将其每个引用直接传递给其容器。
这种差异在于实现,而不是在提供的行为中。EmbeddedReferenceBehavior<>
应该需要更少的交错增量/减量,从而在性能上略有优势。使用 EmbeddedPeristanceBehavior
可以提供更多信息进行调试,因为可以更清楚地了解哪些引用正在持有容器。
提供的构造函数是允许类在运行时查找其容器所必需的。通过在构造期间查找容器,类与容器的类型保持解耦。所有构造函数重载都必须传递容器的指针。
在某些情况下,聚合对象可能还需要容器的其他行为。聚合可以通过另一个指针来访问容器的其他行为,该指针在创建时设置。另一种方法是指定一个更丰富的类作为嵌入式引用行为。在主动对象的后续实现中,容器类型除了引用行为外,还提供回调多路复用行为。那可能看起来像这样
class CActiveObject : public EmbeddedReferenceBehavior< CMessageQueue> { CActiveObject( CMessageQueue *pAnchor) : EmbeddedReferenceBehavior< CMessageQueue>( pAnchor) { } void Start() { // GetReferenceContext() retrieves the message queue indicated // during the constructor. GetReferenceContext()->EnqueueMessage(); } };
ThreadSafeReferenceBehavior<>
策略表示引用计数以线程安全的方式进行。ThreadUNSafeReferenceBehavior<>
也是一个选项,尽管我已经很久没有发现它有用了。它不是线程安全的,但速度更快。这通常用于在单个线程中操作复杂结构的数据处理算法(例如图解析)。对于主动对象,引用计数应该是线程安全的。CFooProxyCount
使用 ThreadSafeReferenceBehavior<>
类两次,第二次是通过 ThreadSafeReferenceCountedObject
typedef
间接使用的。ThreadSafeReferenceCountedObject
中的 ThreadSafeReferenceBehavior<>
表示容器具有线程安全引用计数,第一个 ThreadSafeReferenceBehavior
表示代理引用计数也是线程安全的。
一些示例
在指向代码之前,我还有几个示例要展示。第一个示例展示了我们之前的可聚合对象如何包含在一个引用可计数对象中。第二个示例展示了我们之前的可聚合对象如何包含在另一个可聚合对象中。请注意,第一个构造函数传递了 'this
'。这可能会导致编译器警告。稍后描述的类 CRefCountAnchor
可用于避免孤立并避免警告。
class CReferenceCountableContainer : public ThreadSafeReferenceCountedObject; { CFooProxyCount _foo1; CFooIndirectCount _foo2; public: CAggregateableContainer() : _foo1( this), _foo2( this) { } }; class CAggregateableContainer : public EmbeddedReferenceBehavior< ThreadSafeReferenceCountedObject> { CFooProxyCount _foo1; CFooIndirectCount _foo2; public: CAggregateableContainer( ThreadSafeReferenceCountedObject *pAnchor) : _foo1( pAnchor), _foo2( pAnchor), EmbeddedReferenceBehavior< ThreadSafeReferenceCountedObject>( pAnchor) { } };
在这里,CReferenceCountableContainer
可以像这样在栈上分配。CAggregateableContainer
、CFooProxyCount
、CFooIndirectCount
和 CActiveObject
不能在栈上分配。要在栈上分配这些,可以使用 CRefCountAnchor<>
,并将可聚合类型作为模板参数。CRefCountAnchor<>
专门用于具有 ThreadSafeReferenceCountedObject
类型锚定的对象,请查看其定义以了解如何栈分配具有不同锚定类型的对象。
代码和注释更详细地介绍了这些类以及使用它们的要求。我希望我已经让您了解了其中的内容以及何时使用它们。如果您在此处感到困惑,我建议您查找有关智能指针和引用计数的其他文章。您可能想通读代码,它有很多注释。它还提供了更多使用引用计数对象的示例。