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

boost 2:shared_ptr 包装资源句柄

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (36投票s)

2004年10月4日

15分钟阅读

viewsIcon

273675

downloadIcon

1330

使用 boost,我们可以用很少的代码为 GDI 和其他资源句柄编写“几乎完美”的封装器。

引言

Windows 资源,如 GDI 句柄,如果没有正确管理很容易导致内存泄漏。本文展示了智能指针如何简化这一过程,并几乎消除了主要的错误来源。它是我之前文章《使用智能指针提升你的代码》的延续,提供了一个使用 shared_ptr 的实际示例。(如果你不熟悉 boost::shared_ptr,你应该立即阅读它!)

更新: 虽然第一个版本更像是“概念艺术”,但我稍微打磨了源代码,使其更有用。我添加了一个 reset() 函数,用于将 HandleRef 设置为 0,稍微打磨了源代码,并为 HDC 添加了特殊实现。此外,我还修复了一些代码片段,并在文章中添加了一些内容。

目录

  • 背景 将详细探讨问题,并讨论常见的解决方案。
  • 智能指针解救 将通过一个简单的例子展示智能指针如何在此处提供帮助。
  • 开发解决方案 将把这个想法发展成一组模板类,使重用该想法变得灵活,并允许添加新类型。示例代码讨论了提供的下拉式头文件。
  • 使用解决方案 - 如果你想知道如何使用它,请跳转到这里。
  • CDCRef 用于设备上下文(
  • 讨论

背景

Win32 编程中遇到的许多资源句柄与面向对象环境的契合度不高。这里列出了一些问题:

  1. 你获取句柄的方式决定了是否需要对其调用某种清理函数。如果你使用 CreateFont,则在不再需要它时必须调用 DeleteObject。然而,如果你从窗口获取了 HFONT,则绝对**不能**删除该字体。
  2. 没有办法仅凭句柄本身来判断我们是否应该删除它,还是必须保留它。
  3. 存在许多句柄类型,以及许多不同的 Delete/Release 函数,它们必须精确匹配。
  4. 句柄具有“指针”语义,即拷贝构造函数和赋值运算符仅创建对实际资源的新的引用。出于性能原因,这是可取的,但这也使得在面向对象环境中使用 RAII 变得复杂。

如果你有一个返回此类句柄的函数,你至少必须指定是否以及何时释放句柄。当句柄是类的成员时,事情会变得更加复杂。看看这段无害的代码片段:

class CMessage
{
  protected:
    HFONT   m_font;
  public:

    ~CMessage();
  // ...
};

CMessage CreateMessage(CString const & msgText, 
  LPCTSTR fontName, int fontSize);

代码片段 (1):演示问题

我想问一个问题:CMessage 的析构函数是否应该调用 DeleteObject(m_font)

  • **是?** 那么 CreateMessage 的返回值将为你提供一个已损坏的字体句柄,指向一个已不存在的字体。
  • **否?** 那么谁来删除字体呢?

无论哪种方式,类的用户都需要关心管理你的 HFONT 资源,否则你类的功能将受到严重限制。

存在多种解决方案,有些简单,有些不简单:

  • 禁止拷贝构造函数和赋值运算符(这样的话,你将无法实现 CreateMessage 函数)。
  • 将一个“bool deleteTheFont”标志与字体句柄一起保存。(我曾使用 std::pair<HANDLE, bool> 一段时间,但这仍然很麻烦。)
  • 计算字体的引用次数。(例如,拷贝构造函数必须增加引用计数。)
  • 使用某种内部引用计数。(这适用于文件、事件、线程以及许多其他句柄,使用 DuplicateHandle。然而,这又是另一个麻烦事。GDI 句柄则没有这种运气。)
  • 始终复制对象(复杂、昂贵,有时不可能)。
  • 将上述解决方案之一封装到一个 CFont 类中。

句柄的性质使其成为引用计数机制的理想候选者。

  • 它们充当“资源引用”,但资源本身不能/不应被复制。
  • 相同的资源在许多地方被重用,只要有人在使用它,就不能删除它。
  • 然而,它应该尽快被释放,以便“友好地”与系统资源交互。
  • 这里讨论的资源之间没有相互引用,因此循环引用几乎不会出现。

此外,我们希望保留“非托管”句柄的可能性(即,它不会自动删除)。此标志应在创建智能指针时设置(接近获取资源的地方,因为这是我们知道如何处理它的时候)。

请参阅下文:为什么不用 MFC?,解释了为什么 MFC 的解决方案对我来说不够好。

智能指针解救

如上所述,引用计数的智能指针是处理句柄的理想选择。

为什么 boost::shared_ptr 对此很棒

  • shared_ptr 对资源类型没有任何要求。(我们不需要从某个 CRefCountable 类继承 HFONT。哦,谢天谢地!)
  • shared_ptr 允许使用**自定义删除器**,可以执行特定于资源的清理。(字体用 DeleteObject,句柄用 CloseRegKey 等。)
  • 自定义删除器还允许**不**自动删除资源。

注意:我在上一篇文章中仅简要(或根本没有)提到自定义删除器。请参阅 boost 文档了解更多信息。

让我们来看一个 HFONT 的例子

// first we need a custom deleter for the font:
void delete_HFONT(HFONT * p) 
{ 
  _ASSERTE(p != NULL);   // boost says you don't need  this, 
                         // but lets play safe...
  DeleteObject(*pFont);  // delete the Windows FONT resource
  delete pFont;          // delete the object itself 
                         // (normally done by default deleter)
};

// typedef for our smart pointer:
typedef boost::shared_ptr<HFONT> CFontPtr;

// and a simple "Create" function:
CFontPtr CreateFontPtr(HFONT font, bool deleteOnRelease)
{
  if (deleteOnRelease) {
    // construct a new FontPtr. the custom deleter 
    // is specified as second argument
   return CFontPtr(new HFONT(font), delete_HFONT);  // (A)
  }
  else {
    // construct a new FontPtr with the default deleter:
    return CFontPtr(new HFONT(font));
  }
}

代码片段 (2):初步想法

第 (A) 行是“神奇”的一行:这里我们像往常一样初始化 CFontPtr,但指定当最后一个引用消失时,使用 delete_HFONT 删除该对象。

现在,我们可以大量使用 CFontPtr:我们可以将其用作函数的返回值。我们可以将其作为类成员,并且默认的拷贝构造函数、赋值运算符和析构函数会做它们应该做的一切。

还记得使用智能指针的第一个规则吗?在获取资源后立即将其放入智能指针。此规则在此处也适用:因为当我们获取字体句柄时,我们确切地知道它是否应该被删除。智能指针将携带这个标志,并自动“做我们想做的事”。

即使是这个解决方案也无法解决某些问题:

  • 有人可能会在我们不知情的情况下删除字体。
  • 我们可能创建带有 deleteOnRelease = false 的字体指针,然后忘记自己删除它。

但 C++ 的生活就是这样,你总是可以按你最喜欢的方式“自讨苦吃”。

然而,有些问题可以得到更好的解决:

  • 检查“空”字体很繁琐:我们必须同时检查智能指针和实际字体。
    if (m_fontPtr && *m_fontPtr) // do we have a font?
  • 每次想访问实际对象时,我们都必须解引用智能指针。
  • 对于我们想支持的任何其他句柄类型,我们必须编写三个实体(删除器、智能指针 typedefCreate 函数)。

但这正是下一部分要讨论的问题。

开发完整的解决方案

这一段展示了如何将概念转化为完整且可扩展的解决方案。它可能有助于理解模板库如何演变成复杂的巨兽。

设定的目标如下:

  • 简单地检查 NULL 资源
  • 自动转换为句柄类型
  • 可扩展以支持其他类型
  • 最小化为新类型编写代码的工作量(最好只写一个 typedef
  • 仅头文件定义(没有单独的 .cpp / .lib 文件)

    这个目标实际上是“在路上”设定的,因为 .cpp 文件里没有太多内容,而仅头文件使库更容易重用。

封装

首先,我们封装了智能指针的细节,解决了前两个要求:

class CFontRef
{
  protected:
    typedef boost::shared_ptr<HFONT> tBoostSP;
    tBoostSP m_ptr;

  public:
    explicit CFontRef(HFONT font, bool deleteOnRelease)
    {
      if (deleteOnRelease)
        m_ptr = tBoostSP(new HFONT(font), delete_HFONT);
      else
        m_ptr = tBoostSP(new HFONT(font));
    }

    operator HFONT() const { return m_ptr ? *m_ptr : NULL; }
};

代码片段 (3):迁移到专用类

你可能会(或者应该)注意到以下几点:

  • 公共接口被缩减到最少。(这是“好事”。)
  • CreateFontPtr 函数已变为构造函数。
  • 自动转换运算符允许进行 if (font) 测试,以及在需要 HFONT 的地方使用该类。
  • 删除器仍然是一个关联函数(此处未显示)。
  • 该类从“Ptr”重命名为“Ref”,因为它在语法上更像一个引用而不是指针。
  • 构造函数不指定默认参数。这样做是因为没有一个值是“显而易见的默认值”。此外,这使得构造显式化(所以 explicit 关键字不是真正必需的)。
  • typedef 了类内部的 boost 指针。它在很多地方出现,typedef 使代码更易读,但“类外部”的人实际上不需要它。

另一个注意:现在是进行下面简化的好时机。不过,我希望更“安全且干净地”走得更远。

第二,我们可以将句柄类型和删除器都设为模板参数。对于句柄很简单,但有些编译器无法处理函数作为模板参数。标准的解决方案是将删除器函数变成一个*仿函数*——也就是说,一个重载了 operator() 的类。

template <typename HDL, typename DELETER>
class CHandleRefT
{
  protected:
    typedef boost::shared_ptr<HDL> tBoostSP;
    tBoostSP m_ptr; 

  public:
    CHandleRefT(HDL h, bool deleteOnRelease)
    {
      if (deleteOnRelease) m_ptr = tBoostSP(new HDL(h), DELETER());
      else m_ptr = tBoostSP(new HDL(h));
    } 
    operator HDL() const { return m_ptr ? *m_ptr : NULL; }
}; 

// the font deleter, turned into a functor:
struct CDeleterHFONT
{
    void operator()(HFONT * p) { DeleteObject(*p); delete p; }
}; 

// the typedef for our CFontRef:
typedef CHandleRefT<HFONT, CDeleterHFONT> CFontRef;

代码片段 (4):将类变成模板

DeleteObject 用于多种类型,所以我们不想为每种类型都编写自己的删除器。但是,我们希望保持严格类型化,所以我们再次将其设为模板:

template <typename GDIHANDLE>
struct CDeleter_GDIObject
{
  void operator()(GDIHANDLE * p) { DeleteObject(*p); delete p; }
};

// the typedef now has a nested template:
typedef CHandleRef<HFONT, CDeleter_GDIObject<HFONT> > CFontRef;

代码片段 (5):一个辅助模板

专用、更轻量级的实现

仍有两件事困扰着我:

  • 删除器仍然需要记住“delete p”部分。
  • 如果可能,我希望避免对资源句柄进行堆复制。

第一个问题可以通过另一个辅助模板来解决,但这会将 CFontRef 的实际类型变成类似 boost::shared_ptr<HFONT, GenericDeleter< DeleteObjectDeleter<HFONT> > >。在通用方法中,我看不出第二个问题的解决方案。

然而,当使用一些关于 Windows 的内部知识时,这两者都能解决:Windows 中所有的资源句柄都可以用 void * 表示,我们可以使用强制类型转换来获得严格类型的句柄。这深深植根于 Win32 API(事实上,如果你 #undef STRICT 宏,大多数句柄类型*被声明为* void *),直到 .NET 完全接管,我们都不会看到改变。此外,boost::shared_ptr 可以使用 void 作为模板参数。

所以,使用以下代码,我们可以继续:

// a void deleter replaces the default deleter for "unmanaged" handles
struct CDeleter_Void
{
  void operator()(void *) {}
}

template <typename HDL, typename DELETER>
class CHandleRef
{
  protected:
    typedef boost::shared_ptr<void> tBoostSP;
    tBoostSP m_ptr;

  public:
    explicit CHandleRef(HDL h, bool deleteOnRelease)
    {
      if (deleteOnRelease)
        m_ptr = tBoostSP(h, DELETER());
      else
        m_ptr = tBoostSP(h, CDeleter_Void());
    }

    operator HDL() const { return (HDL) m_ptr.get(); }
};

struct CDeleter_GDIObject
{
  void operator()(void * p) { DeleteObject( (HGDIOBJ) p); }
};

typedef CHandleRef<HFONT, CDeleter_GDIObject> CFontRef;

代码片段 (6):最终代码

之前,智能指针存储了指向我们资源句柄的指针。现在,我们将资源句柄直接存储在智能指针中(表示为 void *)。

使用解决方案

(我将在此处重复一些内容,以防那些跳过所有无聊解释的急性子读者)

CHandleRefT 是一个模板类,实现了 Windows 资源句柄的计数引用。使用规则类似于引用计数智能指针。它用于实现各种 Windows 资源句柄,例如:

  • HIMAGELIST ==> CImageListRef
  • HMENU ==> CMenuRef
  • HANDLE ==> CHandleRef
  • HBITMAP ==> CBitmapRef
  • HBRUSH ==> CBrushRef
  • HPEN ==> CPenRef

模板参数

  • HDL:资源句柄的类型(例如,HFONT)。
  • DELETER:一个释放 HDL 类型句柄资源的仿函数(例如,一个调用 DeleteObject 的仿函数)。句柄以 void * 的形式传递。
    可以向构造函数传递自定义删除器。但是,DELETER 不是类的模板参数(这避免了模板传播)。

自动与手动句柄

从原始句柄构造 HandleRef 时,您需要传递一个 bDeleteOnRelease 标志,该标志指示当对它的最后一个引用超出作用域时是否应自动释放句柄。

对于自动句柄,请在构造后立即将原始句柄分配给 HandleRef,并指定 bDeleteOnRelease=true。然后,仅将其作为 HandleRef 传递。这保证了句柄在其不再使用时会被删除。

对于手动句柄,您将手动删除它,或者它不应被删除,请指定 bDeleteOnRelease=false

这里的想法是,您可以传递、复制、存储或返回 HandleRef,它会记住其删除策略。

准则

仅接收和使用资源句柄(而不存储它)的实用函数可以使用原始句柄作为参数。但是,当句柄被存储(例如作为类成员)或用作函数返回值时,推荐使用 HandleRef

例如,CImageListRef 类具有以下成员:

构造函数

CImageListRef(HIMAGELIST il, bool deleteOnRelease)

初始化一个新的 CImageListRef

  • il [HIMAGELIST]:要保存的图像列表。
  • deleteOnRelease [bool]:如果为 true,则当对图像列表(通过 CImageListRef 实例创建)的最后一个引用超出作用域时,将销毁图像列表 il。将使用正确的销毁函数(ImageList_Destroy)。

operator HIMAGELIST

  • 隐式地将 CImageListRef 转换为包含的 HIMAGELIST

reset()

释放图像列表。

添加对新类型的支持

类型必须可以与 void * 相互转换。

  • 为该类型编写删除器仿函数。
    struct CDeleter_MyType { void operator()(void * p) { MyTypeRelease(p); }
  • 使用 typedef
    typedef CHandleRef<CMyType, CDeleter_MyType> CMyTypeRef;

为什么不用 MFC?

MFC 尝试用自己的封装器来解决这个问题。不幸的是,他们选择了“可能解决方案”中的第一种:禁用复制功能,但他们传递的是 CFont *。这可能曾经是一个好的设计决策,但现在却是个麻烦。考虑一个返回 CFont * 的函数。现在,有两个对象需要正确释放:Windows HFONT 和 CFont C++ 对象。你应该:

  • 完成后删除 CFont *,因为它是在动态分配的吗?
  • 不删除 CFont *,而仅在使用直到另一个类被销毁(因为另一个类“持有”字体)?
  • 不删除字体,而是仅在当前消息处理程序中使用它(因为它是一个临时的 MFC 对象,将在下一个 OnIdle 调用中被删除)?
  • 在删除它之前从 CFont * 中分离 HFONT(因为你必须摆脱 MFC 对象,但 Windows 资源仍在某处使用)?

在将 UI 代码转换为通用例程和类时,我经常会遇到所有这四种情况。我向你保证,这不是一种愉快的体验——所以我最终宁愿使用原始资源句柄,以拥有“只有普通的问题”。

此外,如果可能,我希望 UI 代码不依赖于 MFC。 IMO 最好的代码是一个仅依赖于 Win32 API 接口且不对框架提出任何要求的库。

CDCRef 用于设备上下文

引用 HDCCDCRef 类值得进一步讨论。

设备上下文是我遇到的最复杂资源:有两个清理函数:DeleteDCReleaseDC,后者还需要一个附加参数(获取 DC 的 HWND)。

CDCRef 实现为一个单独的类,因为我不想让 HDC 的复杂性“侵入”其他 HandleRef 类。主要区别在于构造函数——不是使用标志,而是直接传递删除器。

CDCRef(hdc, CDCRef::Null()) // a CDCRef that doesn't get released automatically
CDCRef(hdc, CDCRef::Delete()) // a CDCRef to be released by DeleteDC
CDCRef(hdc, CDCRef::Release(hwnd))  // a CDCRef to be released by ReleaseDC

对于 ReleaseDC,HWND 作为参数传递给删除器。同样,我们将所有必要的清理信息与对象在其构造时关联起来,这是 HandleRef 的设计原则。

此外,CDCRef 提供了封装获取 HDC 的 Win32 API 函数的静态成员函数。例如,CDCRef::GetDC 实现 GetDC Win32 API 函数,但返回一个 CDCRef

讨论

此处提供的解决方案并非新颖。本文的目的是展示当使用合适的库时,解决方案会变得非常简单(实际代码约 20 行),以及这样的解决方案是如何从最初使用 shared_ptr 的想法发展而来的。

那么,这个解决方案有多好?

  • 你需要了解 Win32 资源是如何管理的。
  • 你需要了解智能指针是如何工作的。
  • 它比纯粹的 Win32 代码有轻微的开销:每个托管句柄都有一个单独分配的对象(8 字节),并且清理调用是通过函数指针进行的。

第二个问题不是问题:智能指针是一项如此基本的技术,你不应该不知道。后者在处理大量资源时可能是一个问题。但是,这样的应用程序可能最能从自动资源管理中受益,并且你可以通过在适当的时候使用原始句柄(或 CHandleRefT<> const &)来控制性能。

第一个问题实际上反映了一个*设计选择*:CHandleRef 不会让你脱离底层 API,但它使 API 透明且更易于使用。(相比之下,MFC 在默认情况下将你隔离得很好,但在所有其他情况下都处理得很糟糕)。额外优势:CHandleRef 不仅独立于库,而且易于与其他库集成。

是否存在“更好”的解决方案?

绝对有。你仍然会犯错误:在一个对象被选入设备上下文时释放它,指定错误的清理策略,或者在句柄仍在使用时删除它。然而,一个完全安全的解决方案需要封装整个 GDI API:所有创建或使用 GDI 对象的函数。

CDCRef 实际上展示了*开放式封装器*(允许访问底层 API 句柄)和*封闭式封装器*(拒绝访问)之间的区别。

为了使 CDCRef 的构造“万无一失”,CDCRef 的构造函数必须被保护起来,这样它们只能通过正确初始化的专用函数来构造。但这需要封装所有获取 DC 的函数。此外,为了使 CDCRef 完全万无一失,还需要封装所有接受 DC 的函数,并从类中移除 operator HDC()。开放式封装器让你承担一些责任,但如果我忘记提供封装器(或原始 API 已扩展),你就不会有问题。

结论与感谢

非常感谢大家给予的鼓励反馈!很高兴看到写这篇文章的时间没有白费。

总结来说:我们已经看到:

  • 资源句柄,与其他许多对象一样,可以具有各种销毁策略,而这些策略从句柄本身是“看不见的”。
  • 删除策略应该在获取句柄时“附加”到句柄上。
  • 引用计数的智能指针非常适合封装资源句柄。
  • boost::shared_ptr 提供了使实现简化的功能。
  • 通过利用一些特定于平台的知识,我们可以使解决方案更有效,代码更简单,同时保持原始接口。
© . All rights reserved.