Boost:使用shared_ptr封装资源句柄
boost 2: shared_ptr wraps resource handles(By peterchen)
翻译 masterlee
使用boost,我们可以写出非常完美的包装GDI和其他句柄的代码,而且代码量又很少。
HandleRef class template - 2.72 Kb
Sample project (contains code snippets, and Doxygen documentation) - 43.7 Kb
1、 介绍
在Windows中,对于窗口资源,能够使用如GDI句柄来很好的管理,不会造成内存泄露。本文档将要介绍如何来使用智能指针实现这个操作,并且是其更加简单化,而且消除主要的源码错误。本文档是《在你的代码中使用Boost智能指针》的一个延续,在那里介绍了如何使用shared_ptr的例子(如果你不熟悉boost::shared_ptr,你可以先阅读那篇文章)。
这种解决方案并不是新提出的,这里只是说用适当的库可是使其更简单。编写这篇文章的想法是来源于“我使用shared_ptr能做些什么呢?”。
2、 内容
· 背景将详细调查这个问题,讨论一般解决方案。
· 智能指针的营救在这里用一个例子来说明如何来帮助我们。
· 发展这种方法将利用这种思想开发一组模板库,使这种思想得到重复利用,而且允许加入新的类型。尝试以这个为开始继续编写程序。
· 利用这种方法 —— 这里介绍如何使用这种方法。
3、 背景
在Win32编程中的许多资源句柄都不能够很好的适应面向对象的编程环境。下面列出几个常见的问题:
A、 是否能获得一个句柄要看是否调用了释放资源的函数。如果你使用CreateFont 创建了一个字体,当你不再使用它的时候,调用DeleteObject来释放它,然而如果你从一个窗口中获得HFONT,那么你就不需要释放它。
B、 没有一种方法告诉我们一个句柄不再使用我们应该释放它,或者告诉我们不需要管它。
C、 句柄类型很多,提供了很多Delete/Release函数,需要正确对应使用它们。
D、 句柄有指针用法,例如在使用拷贝构造函数和赋值函数真正创建了一个新的实际资源的引用,虽然这样做性能很好,但是在面向对象的开发中使用RAII将更加复杂化。
如果你有一个函数返回一个句柄,那么你必须指定什么时候释放它。如果这个句柄是类成员变量,那么这件事情就会更加复杂。先看一下这段无错的代码:
class CMessage
{
protected:
HFONT m_font;
public:
~CMessage();
// ...
};
CMessage CreateMessage(CString const & msgText,
LPCTSTR fontName, int fontSize);
这段代码证明了这个问题。
让我们提出一个问题:在CMessage的析构函数中能够调用DeleteObject(m_font)吗?
· Yes?CreateMessage将创建一个字体句柄,但这个字体其实并不存在。
· No?不知道谁会去释放它呢?
不管怎么样,当使用到你的类的时候需要管理里面的字体资源,或者对你的类进行严格限制。
有许多解决方法,有的简单,有的比较麻烦:
· 禁止使用拷贝构造函数和赋值语句(你不能够使用CreateMessage函数)。
· 在字体句柄中需要加一个“bool deleteTheFont”标志(我临时使用了std::pair<HANDLE, bool>,但是还是很麻烦)。
· 使用了一个字体的引用计数器(例如,当使用拷贝构造函数增加字体的时候,引用计数器增加)。
· 使用类似内置引用计数器(这种方法可以在文件、事件、线程和其他句柄使用DuplicateHandle,但是这对于GDI句柄就不试用了,会造成像蠕虫一样展开大量资源)。
· 只使用拷贝对象(但这事非常复杂的,而且消耗资源,有时候不可能实现)。
· 在Cfont类中封装一个方法。
原始句柄在初始化的时候就加入了引用计数器机制:
· 它们担当了“资源的引用”,但这些资源不能进行拷贝。
· 一些资源在许多地方都适用了,所以当使用它的时候不能释放它。
· 对于适用系统资源,应该马上释放。
· 一些资源彼此不能够引用,因此也不会出现循环引用的现象。
在将来,我们会有一些很难管理的句柄(例如,它不能自动释放),这种情况应该使用智能指针来创建。
看后面内容:为什么不使用MFC呢?,为什么MFC不能膀子我们解决这个问题呢?
4、 智能指针的营救
如上面所说,我们可以使用一个引用计数器智能指针来处理句柄。
Boost::shared_ptr的优点:
· shared_ptr不需要任何附加条件可以使用在所有资源类型上(在这里对于HFONT我们就不需要继承CrefCountable类了)。
· 对于一些特定的资源回收,shared_ptr允许使用自己定义的释放操作。(例如,DeleteObject用户释放字体,CloseRegKey用户句柄)。
· 自定义的释放资源操作也允许不去自动删除资源。
注意:在本文提到的自定义释放操作可以参考boost文档。
看下面一个关于HFONT的例子:
// 首先我们自定义一个释放字体的操作
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));
}
}
上面的这段代码只是一个简单的想法。
代码行(A)在这里有点不可思议:这里首先我们用CfontPtr初始化,然后当引用结束,又使用delete_HFONT来释放这个对象。
现在我们再适用CfontPtr:我们用它作为函数的返回值。在类中我们已经有了一个CfontPtr成员变量,和缺省的构造函数,赋值操作和析构函数。
在适用智能指针的第一条规则中我们提高,可以将资源放入智能指针中让我们来适用。这条规则在这里是非常适用的:因为当我们得到一个字体句柄的时候我们需要资到是否应该释放掉,智能指针携带了这个标志,能够自动帮助我们处理这些问题。
下面是使用这种方法不能够解决的问题:
· 别人可以在我们的后面删除这个字体句柄。
· 我们能够创建一个字体指针,而且将deleteOnRelease = false操作加入,那么就会忘记释放。
但这就是C++的精髓所在,你总可以按自己喜欢的方式编写。
对于这些问题,可以这样更好地解决:
· 检查是否有空指针字体:我们需要同时检查智能指针和字体本身。
if (m_fontPtr && *m_fontPtr) // do we have a font?
· 当我们访问字体本身的时候就需要减除智能指针的引用。
· 我们可以写三个操作(释放,智能指针定义,创建函数),这样对于任何句柄我们都可以支持。
但是这样操作对于下面的部分又将是一个问题。
5、 开展一个完整的例子
这部分将这个观念实现为一个完整的可扩展的解决方案。它有助于帮助你理解如何来使用模板库解决复杂的问题。
设置下面几个目标:
· 简单检测空的资源。
· 自动匹配句柄类型。
· 可扩展为其他类型。
· 减少创建新类型的代码量(只使用typedef就可以实现)。
· 只定义头文件(不需要源文件和库文件)
这个目标仅仅是能运行起来,因为这些并不能够加入到源文件中,只有头文件并不能够容易制作成库达到重用的目的。
封装
首先,我们封装一个详细的智能指针,解决前面两个需求:
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; }
};
代码段三:建立了一个类。
应该注意下面几件事情:
· 尽量将公开接口(保持良好的设计)。
· 将CreateFontPtr变成构造函数来实现。
· 自动匹配操作既允许操作字体测试,也可以是用类区操作HFONT。
· 释放操作应该是一个智能函数(不在这里说明)。
· 这个类重命名的后缀由“Ptr”变为“Ref”,只是想让它从表面上看像一个指针的引用。
· 构造函数不指定缺省参数。(这样做是显式缺省,也就是使用了显式构造函数,explicit关键字不是一定要的)。
· 在类中使用typedef来定义boost指针,在很多地方使用typedef,这样可以使代码简单易读,但在类的外面,就不需要这样了。
注意:我们应该花时间将代码简单化,我喜欢示代码更加“安全而且干净”。
下面我们加一个句柄类型河一个带模板参数的释放函数。对于一些简单的句柄,不能够在一个函数中处理模板参数,标准的解决方法是将这个释放函数写成一个仿函数,就是说在类中尧重载operator();
template <typename HDL, typename DELETER>
class CHandleRef
{
protected:
typedef boost::shared_ptr<HDL> tBoostSP;
tBoostSP m_ptr;
public:
explicit CHandleRef(HDL font, bool deleteOnRelease)
{
if (deleteOnRelease) m_ptr = tBoostSP(new HFONT(font), DELETER());
else m_ptr = tBoostSP(new HFONT(font));
}
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 CHandleRef<HFONT, CDeleterHFONT> CFontRef;
代码段四:将类写成模板类。
在于多类型中都使用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;
代码段五:一个协助模板。
专业的、轻量级的封装
这里有两件事情仍然困扰着我们:
· 每个释放函数里仍然要记住删除谁,因为使用了“delete p”。
· 尽可能地避免堆栈的资源拷贝。
第一个问题可以使用另一个协助模板来解决,但是它不能像boost::shared_ptr<HFONT, GenericDeleter< DeleteObjectDeleter<HFONT> > >来使用CfontRef类型。第二个方案可以解决我们的问题。
但是药解决它们需要一些关于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(HFONT font, bool deleteOnRelease)
{
if (deleteOnRelease)
m_ptr = tBoostSP(font, DELETER());
else
m_ptr = tBoostSP(font, CDeleter_Void());
}
operator HDL() const { return (HDL) m_ptr.get(); }
};
struct CDeleter_GDIObject
{
void operator()(void * p) { DeleteObject(p); }
};
typedef CHandleRef<HFONT, CDeleter_GDIObject> CFontRef;
代码行六:最终代码
之前,智能指针存储了以一个指向资源句柄的指针,现在我们可以将资源直接存储为一个智能指针(描述成void *类型)。
6、 利用这种方法
(在这里我将使用一些东西代替那些繁琐的内容。)
ChandleRefT 是一个操作一个引用计数器指向Windows资源句柄的模板类,用这种定义方法可以定义类似的引用计数器智能指针。经常食用的Windows资源句柄,都像下面转换:
HIMAGELIST ==> CImageListRef HMENU ==> CMenuRef HANDLE ==> CHandleRef HBITMAP ==> CBitmapRef HBRUSH ==> CBrushRef HPEN ==> CPenRef模板参数
HDL:资源句柄类型(如HFONT) DELETER:一个释放HDL的仿函数(如一个调用DeleteObject的仿函数)。这个句柄可以传递void *类型。自动操作句柄和手动操作比较
当通过一个原始句柄构造一个HandleRef的时候,你需要通过bDeleteOnRelease这个标志来设定当引用已经离开它的作用域的时候去自动释放。
对于一个自动句柄,在通过一个原始句柄构造一个HandleRef时,需要立刻指定bDeleteOnRelease = true, 然后通过它来食用HandleRef,这样保证句柄不再使用的时候会被释放掉。
对于一个手动句柄,需要你自己手动去释放它,或者不需要释放的话,需要指定bDeleteOnRelease = false。
使用这个方法,你可以拷贝,存储,或者返回一个HandleRef类型的句柄,但一定要知道它的释放策略。
指导方针
函数的作用仅仅接收使用一个资源句柄(不能存储它)或者使用原始句柄作为参数。因此想存储一个句柄(如作为一个类)或者想让函数有返回值得时候,请使用HandleRef。
例如,CimageListRef类有下面成员函数的实现:
7、 构造函数
CimageListRef(HIMAGELIST il, bool deleteOnRelease)
初始化一个新的CimageListRef。
il[HIMAGELIST]:存储一个图形列表 deleteOnRelease [bool]:如果是true,当最后一个引用离开它作用域的时候il自动释放(通过CimageListRef实例化的)。使用正确的释放函数(ImageList_Destroy)。operator HIMAGELIST
隐式匹配CimageListRef到一个包含HIMAGELIST 的类型。增加支持新的类型
这种类型必须能转换到void*类型
为这个类型写一个释放资源的仿函数。struct CDeleter_MyType { void operator()(void * p) { MyTypeRelease(p); }
使用typedef。typedef CHandleRef<CMyType, CDeleter_MyType> CMyTypeRef;
8、 为什么不用MFC?
MFC尽力使用自己的封装来解决这些问题,但遗憾的是,它并不能像上面介绍的那样尽可能的解决:不进行函数拷贝可以使用CFont *传递。对于后面这可能是一个好的设计,但现在是不恰当的,如果想在函数返回一个CFont *类型,你应该这样做:
当你使用完毕后一定要删除CFont *,因为它是动态分配的。 不能删除CFont *,只有在一些类销毁它后才能操作(因为其他类也拥有它)。 不能删除字体,但在当前消息句柄中能够使用(因为作为一个临时的MFC对象只能在下一个OnIdle调用的时候释放)。 在CFont *删除之后还能获取HFONT(因为你虽然将MFC对象释放了,但Windows资源在一些地方仍然使用)。当将UI操作代码转换到通用运行时和类的时候,按照上面的四条原则进行转换,但并不是那样顺利,当将所有的原始资源句柄都替换掉的时候,还有一些常规的问题需要解决。
如果可能的话,我希望UI操作代码不要依赖MFC,最好的代码应该是建立在Win32 API接口上,不需要任何框架。
9、 结论
总结一些结论:
对于建立了许多对象的资源句柄,应该有多种释放策略,本应该直接叫用户操作句柄本身。 释放策略应该依附在使用的句柄上。 引用计数器智能指针能够完美地封装资源句柄。 boost::shared_ptr提供了大量特征可是使操作更简单。 在使用一些于平台相关的东西的时候,尽力使用原始接口不要负责的代码,这样可以使我们的处理更有效。10、 资源
获取更多的信息,或者有问题可以查找如下资源:
· Boost home page
· Download Boost
· Smart pointer overview
· Boost users mailing list
· Boost中的智能指针(撰文 Bjorn Karlsson 翻译 曾毅)
郑重声明:
允许复制、修改、传递或其它行为
但不准用于任何商业用途.
写于 2004/11/7 masterlee
在你的代码中使用Boost智能指针(1)
在你的代码中使用Boost智能指针(2)
本文地址:http://com.8s8s.com/it/it25715.htm