促进 C++库

……其中一个世界。 赫伯·萨特安德烈亚历山德雷斯库,C类++编码标准

计数身体技术

亨尼
(kevlin@curralan.com)

参考计数技术?你可能会想,没有什么新鲜事。每一个好的C++文本都会把你带到一个中间或高级水平将引入这一概念。它已经被探索过了过去如此彻底,你可能会被原谅因为我认为所有可以说的都已经说了。好吧,让我们从第一原则开始,看看我们是否可以发掘新事物。。。。

然后就没有了。。。

参考计数的原理是保持一个对象的使用计数,这样当它降到零时我们就知道了对象未使用。这通常用于简化动态分配对象的内存管理:保持保存到该对象的引用数,以及零,删除对象。

如何跟踪对象的用户数量?好吧,普通的指针很蠢,所以需要额外的需要间接来管理计数。这是基本上是中描述的代理模式设计模式[Gamma、Helm、Johnson&Vlissides,Addison-Wesley,ISBN 0-201-63361-2]。意图如下所示

为另一个对象提供代理项或占位符控制对它的访问。

Coplian公司[高级C++编程风格和习语,Addison-Wesley,ISBN 0-201-56365-7]定义了一组习惯用法与把手和身体的本质分离有关部分。这个Taligent程序设计指南[Addison Wesley,ISBN 0-201-40888-0]确定了代理(又称代理)的特定类别。广泛地说起来,它们分为两大类:

  • 隐藏:句柄是感兴趣的对象,隐藏尸体本身。手柄的功能是通过委托给机构获得,以及手柄不知道身体。引用计数字符串提供透明的优化。尸体由以下各方共享复制字符串,直到需要更改时为止,在这一点上进行复制。这样的COPY ON WRITE模式(LAZY EVALUATION的专门化)需要使用隐藏引用计数正文。
  • 明确的:这里的尸体很有趣句柄仅为其访问和家务。在C++中,这通常被实现为SMARTPOINTER习语。一个这样的应用程序是引用计数的智能指针,它们协作以保持对象,当计数降至零时将其删除。

连接与分离

对于引用计数的智能指针,有两个位置计数可以存在,导致两种不同的模式概述于软件模式[科普连、SIGS、ISBN0-884842-50-X]:

  • 计数主体或附加计数把手/主体放置计数对象内的计数。好处是可数性是被计数对象的一部分引用计数不需要额外的对象。这个缺点很明显,这是侵入性的,而且当对象为不是基于堆的。因此,参考计数将您与特定的实现和使用风格。
  • DETACHED COUNTED HANDLE/BODY将计数置于被计数的对象,以便将它们一起处理。这一技术的明显好处是完全不引人注目,拥有所有的智慧和支持智能指针中的设备,因此可以用于独立于引用创建的类计数指针。主要缺点是频繁使用这可能导致小物体的扩散,即。在堆上创建的计数器。

即使有了这个简单的分析,似乎DETACHED计数手柄/车身进近在前方。事实上,随着增加模板的使用这通常是最受欢迎的共同但非标准背后的原则-计数_ptr.[Boost的名称是共享ptr而不是计数_ptr.]

COUNTED BODY的常见实现是提供计数类型为的基类中的计数机制派生自。要么是这样,要么是引用计数机制为每个需要它的类重新提供方法不令人满意,因为它们非常封闭,将类耦合到特定框架中计数处于休眠状态的非内聚性非计数对象,你会觉得除了它在COM和CORBA等广泛的对象模型中使用COUNTED BODY方法可能仅用于专业情况。

基于需求的方法

正是开放的问题说服了我重新审视COUNTED BODY习语的问题。是的,有一个使用这个习语时,预期会有一定程度的侵入,但是有没有办法最大限度地减少这种情况,并使使用的智能指针类型的计数机制?

近年来,最具指导意义的代码和开放式通用组件构造规范已成为Stepanov和Lee的STL(标准模板库),现在是C++标准库的一部分。STL公司该方法广泛使用了基于编译时多态性的关于明确的类型操作要求。对于实例,每个容器、包含的和迭代器类型为由应在上执行的操作定义该类型的对象,通常带有描述其他约束。编译时多态性,作为其名称建议,基于函数在编译时解析函数名称和参数用法,即重载。这就少了侵入性,尽管如果不正确,则不太容易诊断基于类型、名称和函数的运行时多态性签名。

这种基于需求的方法可用于参考计数。类型需要的操作可数的松散:

  • 获得注册利息的操作可数的对象。
  • A类释放操作未注册权益可数的对象。
  • 获得返回是或的查询不是可数的对象当前已获取。
  • A类处置负责的操作处理不再获取的对象。

请注意,计数是作为摘要的一部分推导出来的此类型的状态,并且在任何另一种方式。这种方法的开放性部分源于使用全局函数,意味着没有特定成员隐含功能;包装现有产品的完美方式计算主体类,而不修改类本身。这个开放的另一个方面来自于更精确的操作规范。

对于要成为的类型可数的它必须满足以下要求,其中脉冲重复频率是非null指向该类型的单个对象(即非数组)的指针,以及#功能指示呼叫的次数功能(脉冲重复频率):

表达式 返回类型 语义和注释
获取(ptr) 无要求 邮递:收购(ptr)
释放(ptr) 无要求 之前:收购(ptr)
邮递:已获取(ptr)==#获取-#释放
收购(ptr) 可转换为布尔 返回:#获取>#释放
处置(ptr,ptr) 无要求 之前:!收购(ptr)
邮递:*脉冲重复频率不再可用

请注意,的两个参数处置是到支持选择适当的类型安全版本要调用的函数。一般情况下,其目的是第一个参数确定要删除的类型通常是模板化的,而第二个选择哪个模板使用,例如通过符合特定的基类。

此外,还必须满足以下要求满意,其中无效的是指向可数的类型:

表达式 返回类型 语义和注释
获取(空) 无要求 行动:无
释放(空) 无要求 行动:无
已获取(空) 可转换为布尔 返回:
处置(null,null) 无要求 行动:无

请注意,在抛出或未抛出异常的条件,除非抛出异常,函数本身应该是例外安全。

变得聪明起来

鉴于可数的类型的要求,它是可以定义使用它们的通用智能指针类型参考计数:

模板<typename countable_type>类countable_ptr{公共://建设和破坏显式countable_ptr(countable_type*);countable_ptr(常量countable-ptr&);~countable_ptr();public://访问countable_type*运算符->()常量;countable_type&运算符*()const;countable_type*get()常量;public://修改countable_ptr&clear();countable_ptr&assign(可数类型*);countable_ptr&assign(常量countable-ptr&);countable_ptr&运算符=(常量countable_ptr&);私有://表示countable_type*body;};

该类的接口已被有意保留简单,例如成员模板和规格有为了说明,省略了。大多数功能是实现非常简单,非常依赖分配成员作为keystone函数:

模板<typename countable_type>countable_ptr::countable-ptr(countable_type*initial):正文(首字母){收购(主体);}模板<typename countable_type>countable_ptr<countable_type>::countable_ptr(const countable-ptr&other):body(其他.body){收购(主体);}模板<typename countable_type>countable_ptr(){清除();}模板<typename countable_type>countable_type*countable_ptr::operator->()const{返回体;}模板<typename countable_type>countable_type和countable_ptr<可数类型>::运算符*()常量{返回*主体;}模板<typename countable_type>countable_type*countable_ptr::get()const{返回体;}模板<typename countable_type>countable_ptr<countable_type>和countable_ptr<countable_type>::清除(){返回赋值(0);}模板<typename countable_type>countable_ptr<countable_type>&countable-ptr<可数类型>::assign(可数类型*rhs){//设置为rhs(使用自动分配安全的“发布前复制”习惯用法)获取(rhs);countable_type*old_body=正文;身体=rhs;//整理释放(old_body);if(!已获取(old_body)){处置(old_body,old_bodys);}返回*this;}模板<typename countable_type>countable_ptr<countable_type>&countable-ptr<可数类型>::assign(const countable.ptr&rhs){返回赋值(rhs.body);}模板<typename countable_type>countable_ptr<countable_type>&countable-ptr<coountable_type>::operator=(const countable.ptr&rhs){返回赋值(rhs);}

公共问责制

符合要求意味着类型可以是与一起使用可数_ptr。这是一个实现混合类(混合输入)使其具有可计数性通过成员函数派生类。这个类可以是用作类适配器:

类可数性{public://操纵void acquire()常量;void release()常量;size_t获取的()常量;受保护://构造和破坏可数性();~countability();私有://表示可变大小计数;私有://预防可数性(常数可数性&);可数性&运算符=(const-countability&);};

请注意,操作函数是常数而且那个计数成员本身就是可变的。这是因为可数性是不是对象抽象状态的一部分:内存管理不依赖于常数-性质或其他对象。我不会包括成员的定义函数如下所示:增量,并分别返回当前计数操作功能。在多线程环境中,您可以应确保此类读写操作原子。

那么我们如何制作这个类可数的? 一个简单的集合的转发功能执行以下操作:

无效获取(常数可数性*ptr){if(ptr){ptr->acquire();}}无效释放(常数可数性*ptr){if(ptr){ptr->release();}}已获取size_t(常量可数性*ptr){返回ptr?ptr->acquired():0;}模板<类countability_derived>无效处理(const countability_derived*ptr,const coountability*){删除ptr;}

现在派生自的任何类型可数性可以现在与一起使用可数_ptr:

类示例:公共可数性{。。。};void simple(){countable_ptr<example>ptr(新示例);countable_ptr<example>qtr(ptr);ptr.clear();//将ptr设置为指向null}//qtr销毁时删除分配的对象

运行时混合

挑战是在非侵入式中应用COUNTED BODY时尚,这样当一个对象不是计数。我们想做的是将此能力授予按对象而不是按类。实际上,我们在之后可计算性在任何对象上,即任何对象上空隙*! 不言而喻那个无效可能是所有人中承诺最少的类型。

可以说,解决这一问题的力量非常有趣最少。有趣,但并非不可逾越。鉴于此运行时对象的类不能在任何井中动态更改定义方式,并且对象的布局必须固定,我们必须找到新的地点和时间来添加计数状态。必须仅在创建堆时添加这一事实表明以下解决方案:

结构countable_new;外部常量countable_new-countable;void*运算符new(size_t,const countable_new&);void运算符delete(void*,constcountable_new&);

我们超载了操作员新用假人将其与常规全局变量区分开来的论点操作员新这与使用标准::nothrow_t类型和标准::nothrow对象。放置操作人员删除如果发生以下情况,有没有进行清理施工失败。请注意,这还不是所有的那么多编译器。

结果新的表达式使用可数的是在堆上分配的对象有一个包含计数的头块,即我们已经扩展了通过给对象添加前缀。我们可以提供一些特性在实现中的匿名命名空间(未显示)中用于支持计数及其从原始数据访问的文件指针:

结构计数{size_t值;};count*标头(const void*ptr){返回const_cast<count*>(static_cast<const-count*>(ptr)-1);}

这里需要注意的一个重要约束是计数应适当对齐适用于任何类型。对于所示的定义,情况如下几乎所有平台。但是,您可能需要添加填充成员,例如使用匿名联盟联合计数而且是最多的对齐类型。不幸的是,没有便携式的指定此项,以便最小对齐也为观察到-这是指定自己的时常见的问题不直接使用这两种结果的分配器新的malloc公司.

再次注意,计数不被视为对象的逻辑状态,因此从常数到非-常数-计数实际上是可变的类型。

分配器函数本身相当简单明了:

void*运算符new(size_tsize,const-countable_new&){count*allocated=static_cast<count*>(::operator new(sizeof(count)+size));*分配=count();//初始化收割台分配的收益+1;//调整结果以指向车身}void运算符delete(void*ptr,const countable_new&){::运算符delete(标头(ptr));}

给定一个正确分配的标头,我们现在需要可数的要操作的功能恒定空隙*完成图片:

无效获取(const void*ptr){if(ptr){++标题(ptr)->值;}}空隙释放(const void*ptr){if(ptr){--标题(ptr)->值;}}已获取size_t(const void*ptr){返回ptr?标题(ptr)->值:0;}模板<typename countable_type>void dispose(const countable_type*ptr,const void*){ptr->~countable_type();操作符删除(const_cast<countable_type*>(ptr),countable);}

其中最复杂的是处置必须确保销毁正确类型的函数并且从正确的偏移量中收集内存。它使用第一个参数的值和类型来执行此操作正确的,第二个论点只是一种策略选择器,即使用恒定空隙*将其与之前显示的处理区分开来常量可数性*.

变得更聪明

现在我们有了一种在创造时增加可数性的方法对于任何类型的对象,需要什么额外的东西来实现这一点使用可数_ptr我们之前定义的?很好新闻:没什么!

类示例{。。。};void simple(){countable_ptr<example>ptr(新(可计数)示例);countable_ptr<example>qtr(ptr);ptr.clear();//将ptr设置为指向null}//qtr销毁时删除分配的对象

这个新(可计数)表达式定义了不同的分配和释放策略,以及对于其他分配器,任何混合分配的尝试策略,例如呼叫删除在分配的对象上具有新(可计数),导致未定义行为。这与混合时的情况类似新[]具有删除malloc公司具有删除.整个要点属于可数的一致性是指可数的对象与一起使用可计数_ptr,还有这个确保正确使用。

然而,事故会发生,你不可避免地会忘记使用分配新(可计数)而是使用新的。此错误和其他错误可能是在大多数情况下通过扩展此处显示的代码来检测的检查成员计数,验证检查在每一条通道上。确保两者之间明确分离的好处头文件和实现源文件意味着您可以引入此分配器的检查版本,而不需要重新编译代码。

结论

本文有两个关键概念介绍:

  • 使用基于通用需求的方法简化并调整COUNTED BODY模式的使用。
  • 通过控制分配动态和非侵入性地向fixed添加功能使用RUNTIME MIXIN模式键入。

两者的结合应用产生了一种新的基本计数身体模式的变体,UNITRUSIVE计数机构。你可以把这个主题更进一步,设计一个简单的C++垃圾收集系统。

的完整代码可数_ptr,可数性、和可数新也可以使用。

首次发表于 超载 1998年4月25日,ISSN 1354-3172