65.9K
CodeProject 正在变化。 阅读更多。
Home

支持多态类型的引用计数指针类

starIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIconemptyStarIcon

1.78/5 (6投票s)

2002 年 10 月 4 日

CPOL

5分钟阅读

viewsIcon

121014

downloadIcon

625

支持多态类型的引用计数指针类

目标

由多个线程使用的对象的内存管理经常会导致访问冲突,因为一个线程可能在另一个线程仍在对其使用时将其删除。这导致了通过至少存在一个对对象的引用来定义对象的生命周期的想法。

设计要求

  • 用法语法:由于指针类是要向后兼容现有代码的,因此在代码中所需的更改应尽可能少。所以智能指针的用法应该尽可能接近原始指针的用法语法。
  • 多态支持:大多数系统使用抽象类来定义由多个类继承的接口。这是为了将接口与实现分开。在这样的系统中,必然会有接口指针的容器。

在多态对象中,不同接口指针的地址是不同的。考虑一个类 MyClass 继承自 IinterfaceAIinterfaceB

  • 问题 1:指向同一对象的智能指针可能具有不同的类型。
  • 问题 2:有些类型可能是抽象的,并且在没有切片的情况下调用这些对象的 delete 会导致问题。
  • 问题 3:指向同一对象的指针将具有不同的值。
MyClass* pClass = new MyClass;
IInterfaceA* pIntA = pClass;
IInterfaceB* pIntB = pClass;
在此示例中,pClasspIntApIntB 指向同一对象,但它们的值不同。为什么?因为每个接口在内存中的偏移量都不同。

解决方案

首先考虑问题 3。如何获取对象的起始地址?C++ 标准规定 dynamic_cast<void*> 始终指向对象的起始位置。如果先对地址应用此转换,然后再更新基于地址的引用计数或执行比较操作,问题 3 就解决了。

但是,真的解决了?不是,因为 dynamic_cast 仅适用于多态类型。对于非多态类型,代码将无法编译。因此,dynamic_cast 应该仅在对象是多态类型时执行。运行时类型信息 (RTTI) 可用于此目的。有一个关键字 typeid,它在运行时评估一个表达式,仅当它是多态类型时。使用逗号表达式如下:

// Template function because the type that comes in is 
// not known at compile time
template <typename U>
bool is_polymorphic(U* p)
{
   bool result = false;
   typeid(result=true, *p);
   return result;
}
它是如何工作的?查看表达式 (result=true, *p)。此表达式始终返回 *p,即最右侧的值。关键字 typeid 仅当类型是多态类型时才评估表达式。因此,仅当 p 指向多态类对象时,result 才为 true。对象的起始地址也可以类似地获得。
// Template function because the type that comes in is not 
// known at compile time
template <typename U>
void* get_pm_ptr(U* p)
{
   void* x = static_cast<void*>(p);
   typeid(x = dynamic_cast<void*>(p), *p);
   return x;
}
正如您可能推断出的,仅当对象是多态类类型时,才会执行 dynamic_cast。
但是,还有一个问题。如果 p 是 NULL,则 typeid 运算符会抛出异常。因此,
// Template function because the type that comes in is not 
// known at compile time
template <typename U>
void* get_pm_ptr(U* p)
{
   void* x = static_cast<void*>(p);
   if (x != NULL)
     typeid(x = dynamic_cast<void*>(p), *p);
   return x;
}

最后,为了解决问题 1 和 3,引用计数应该根据地址而不是类型来维护。这只能在全局范围内完成。我们创建一个静态对象,该对象维护一个地址到其引用计数的映射。指针类使用此静态对象来更新引用计数,并确定是否应删除该对象,因为引用计数为零。

问题 2:删除问题:如果智能指针是incomplete type 或指向继承类之一,那么如果类没有实现虚析构函数,就无法进行删除。因此,提供了一个 IDelete 接口类。它有一个纯虚函数。我们强制所有需要与此智能指针一起使用的多态类实现此接口 - 如果它没有虚析构函数。纯虚函数名为 self_delete()。在您的多态类中 self_delete 的实现应如下所示:
public:
virtual void self_delete()
{
   delete this;
}
智能指针现在将调用 self_delete,在将其转换为 IDelete 类型后,如果对象实现了 IDelete;否则,将执行原始删除。

警告:如果您没有为没有虚析构函数的多态类实现 IDelete ,则行为是未定义的。

用法语法

通过重载 =、->、==、!、!=;实现接受原始指针的构造函数;并实现智能指针复制构造函数,可以保留原始指针的用法语法。唯一的妥协是 C++ 转换运算符 - static_castdynamic_cast 等。
// myclass inherits from iinterface1 and iinterface2
_mm::_ptr<myclass> x = new myclass; 

// the following line will not compile
_mm::_ptr<iinterface1> y = dynamic_cast(x); 
// the correct usage is
_mm::_ptr<iinterface1> y = dynamic_cast(x.ptr()); 

依赖项

代码应在“项目 | 设置 | C/C++ 选项卡 | C++ 语言”中启用“启用 RTTI”选项进行编译。
它使用了 STL map 类。

在您的代码中使用

在您的项目中包含 smart_ptr.h。该类名为 _mm::_ptr ,用法如下:
// myclass inherits from iinterface1 and iinterface2
_mm::_ptr<myclass> x = new myclass; 
// ref count for object is now 1
_mm::_ptr<iinterface1> y = dynamic_cast<iinterface1* >(x.ptr()); 
// ref count for object is now 2
_mm::_ptr<iinterface2> z = dynamic_cast<iinterface1* >(x.ptr()); 
// ref count for object is now 3

z = dynamic_cast<iinterface1* >(x.ptr();                
// ref count is still 3. this takes care of circular references.
z = NULL;                           // ref count is 2
y = NULL;                           // ref count is 1
x = NULL;                           // ref count is 0 - object deleted

测试

您可以使用测试项目进行一些性能测试。我在 PII/266 的测试中,赋值操作大约需要 47 纳秒。请给我改进的建议。特别是,欢迎任何代码优化尝试<CODE>。

错误修复/更新

  1. 添加了一个 IDelete 接口,以防止在处理具有多个继承类的对象时出现切片问题。
  2. 更改了删除方式,如果不存在 IDelete 的实现,则直接删除对象。这是必需的,因为 RTTI 将多态性解释为存在一个虚函数,而大多数类都只有一个虚析构函数。
  3. 添加了一个函数 ptr() 来返回原始指针,以便可以在不分配给原始指针的情况下执行 dynamic_cast
  4. 添加了关键部分,使全局映射访问成为线程安全的。

致谢

正如 **William Kempf** 指出的,如果您的类具有虚析构函数,则不需要 IDelete 接口。如果没有,问题不是切片,结果是未定义的。我保留了 IDelete ,以防有人选择将此类与没有虚析构函数的多态类一起使用。

**Greg Vogel** 指出了缺失的复制赋值运算符。

他还指出,指针不能跨模块边界传递,因为每个模块都使用静态映射的副本进行工作。Greg 建议使用内存映射文件来解决此问题,他已经成功实现了它。

更深入地考虑这个问题,似乎将指针传递给其他模块或库的理想方法是将其作为原始指针传递。主应用程序应该维护一个引用计数的指针,直到模块需要该原始指针。建议模块不要在内部存储来自智能指针的原始指针,而是在每次函数调用时从主应用程序获取它们。这是引入原始指针支持的最初原因。对此前提的任何更改都必须由该类的用户来实现。

**Chen Sheng** 对代码进行了广泛测试,并为修复代码中的错误做出了宝贵贡献。

参考文献

© . All rights reserved.