有效的使用和设计COM智能指针——条款20:安全的覆盖掉C++默默为我们编写的函数
2011-09-21 10:48
926 查看
条款20:安全的覆盖掉C++默默为我们编写的函数
更多条款请前往原文出处:http://blog.csdn.net/liuchang5我们先试着写一个最小的类,它的代码中不包含任何成语函数和实现,因此看上去是这样的:
class Empty{};
看上去他空空如也,但其实并非如此。C++编译器会默默为我们填写上如下几个成员函数,用以完成一个简单类所应具备的“日常”功能。上面的Empty成了如下这般模样:
class Empty{ public: Empty(){ ... }; //默认的构造函数 Empty(const Empty& rhs){ ... }; //拷贝构造函数 ~Empty(){ ... } //析构函数 Empty& operator=(const Empty &rhs) //赋值运算符 };
这些这是我们设计和编写智能指针代码时候应当时刻提防的地方。编译器确实为我们编写类减轻了负担,但这些函数也为我们犯下错误埋下来隐患。但是对待它们的态度却应该是这样的:
1.如果你不需要某个编译器为我们生成的函数,我们应当显示的拒绝他,将他的访问权限设置为私有。
2.如果编译器默默为我们编写的函数不符合你的需求,那么我们应当重写他。
首先第一个问题的回答是:设计COM智能指针,这这四个函数,我们都需要。因此你可能不必考虑需要将某个成员函数隐藏起来的问题。
而第一个问题的回答加重了我们回答第二个问题的负担,在智能指针中我们需要如何去覆盖这4个函数?
首先如果涉及到资源管理问题,那么应当想到的是析构函数中能否正常的释放所持有的资源。因此析构函数应当呈现出如下形式:
~CMySmartPtr() { if (p) p->Release(); }
看似没有问题,但是你却不保证p->Release()不会抛出一个以后,这一特点决定于COM组件的实现者。那么在析构函数这抛出异常,会导致何种结果呢?回答是行为不确定,有些情况下程序会直接崩溃,而有些情况资源泄漏偷偷发生了【4】。
当你用智能指针来解决资源泄漏问题之时,或许会对这一问题慎重对待。有两种方式可以解决它。
~CMySmartPtr() throw() //加上一个trhow() ,让它在抛出异常的时候直接让程序崩掉 { if (p) p->Release(); }
这是个粗糙的解决办法,但它确实解决了资源泄漏的问题。我更加喜欢下面这种做法:
~CMySmartPtr() throw() { try { if (p) p->Release(); } catch(...) { ... //将异常记录下来,或做相应的处理。 std::abort(); } }
以上这种方式catch到异常后,你可以选择将异常“吞掉”。但那样不便于尽早的发现错误。当然最好的方式还是做完相应的记录和处理后,添加一个std::abort语句,以便让程序中的错误尽可能早的暴露出来。
或许你会觉得throw()和std::abort()略微显得有些冗余。但他们并不会给编译后的程序增加太多冗余的代码。而使用者却能在接口出清楚的知道:这是个本本分分将自己的析构工作完成,而不会给调用者抛出“麻烦”的析构函数。
再来看看指针被拷贝时候的拷贝构造函数。当拷贝一个智能指针之时,应该增加引用计数。这个确实不难,但请注意指针为空的情况:
CMySmartPtr(const CMySmartPtr<T>& lp) { if ((p = lp.p) != NULL) p->AddRef(); }
你若觉得赋值运算符将和拷贝构造函数会有相似的形式,而只是在此基础上简单的加上将原来指针资源释放掉的过程,那就大错特错了。因为这样一个函数可能需要我们考虑更多的问题。
再明确这些问题之前,首先看看一个错误的版本:
T* operator=(const CMySmartPtr<T>& lp) throw() { if (m_pI != NULL) { m_pI->Release(); } if ( lp.m_pI != NULL) { lp.m_pI->AddRef(); } m_pI = lp.m_pI; }
这个做法看似没有问题,但她却忽略了一个事实:智能指针可能自我赋值。试想一下如果程序员写出下面这种代码会发生什么?
CMySmartPtr<IMyInterface> spIMyInterface = NULL; spIMyInterface.CreateInstance(CLSID_MYCOMPONENT); spIMyInterface = spIMyInterface; //出现了自我赋值,程序崩溃了。
首先m_pI将引用计数释放,此时可能由于引用技术归0,COM组件被系统回收。但之后呢?由于自我赋值时,m_pI和lp.m_pI都指向同一个组件,那么伴随着lp.m_pI->AddRef()的调用,程序崩溃了!
确实是个严重的错误,于是你这样来编写这段代码(事实上ATL就是这样编写的):
T* operator=(const CMySmartPtr<T>& lp) throw() { if ( lp.m_pI != NULL) { lp.m_pI->AddRef(); } if (m_pI != NULL) { m_pI->Release(); } m_pI = lp.m_pI; }
看似没有问题,但他却潜藏了一个容易让程序发生错误的地方:它并非线程安全的。
我们从线程会切换这一事实上看认真分析一下这段代码。为了再现这种隐晦的问题,或许我用下面这个表格来表述会更好一些。垂直方向表示时间,左右标识两个不同的线程。
spIMyInterface = spIMyOtherInterface; //调用operator= { if ( lp.m_pI != NULL) { lp.m_pI->AddRef(); } if (m_pI != NULL) { m_pI->Release(); //时间片用完了 } m_pI = lp.m_pI; } | //暂时时间片还为分配过来 //此线程已经持有了一个spIMyInterface //的智能指针。 //线程时间片分配过来了 spIMyInterface->DoSomeThing(); |
T* operator=(const CMySmartPtr<T>& lp) throw() { IUnknown *pOld = m_pI; //先保存一下原先的接口指针 m_pI = lp.m_pI ; //完成赋值与引用计数操作 if ( lp.m_pI != NULL) lp.m_pI->AddRef(); if (pOld != NULL) //再来处理原来保存的智能指针 pOld ->Release(); return m_pI; } |
T* operator=(const CMySmartPtr<T>& lp) throw() { if (m_pI != lp.m_pI) //考虑到自我复制问题的operator= { IUnknown *pOld = m_pI; //先保存一下原先的接口指针 m_pI = lp.m_pI ; //完成赋值与引用计数操作 if ( lp.m_pI != NULL) lp.m_pI->AddRef(); if (pOld != NULL) //再来处理原来保存的智能指针 pOld ->Release(); } return m_pI; }
可能还会继续说,这样做还是会出现线程切换后的空指针错误。是的,但是现在当资源释放后,指针已经置空。这表明你使用了一个已经被释放的资源,因此你得考虑重新审视你的代码,并做出修改。而不陷入到智能指针带来的一种未确定的危险行为之中。更重要的是,这种做法使得一次操作中智能指针的状态始终保持了一致性。
最后只剩下默认构造函数了,或许它是编译器为智能指针编写的最合适的一个函数。但出于安全考虑我们还是给给它加入了一个简单的初始化工作。看起来像是下面这种形式:
CMySmartPtr() : m_pI(NULL) {}
这一小节先到此结束吧,之后还会有更多的函数等待我们加入。
相关文章推荐
- 有效的使用和设计COM智能指针——条款22:果断放弃二进制重用,而采用模版编写智能指针
- 有效的使用和设计COM智能指针——条款11:以类型安全的方式创建资源和查询接口
- 有效的使用和设计COM智能指针——条款17:重载运算符时应当符合C/C++约定
- 有效的使用和设计COM智能指针 条款8:条款9:尽可能不将智能指针放置于堆上
- 有效的使用和设计COM智能指针——条款18:重载运算符不应当扭曲其语义
- 有效的使用和设计COM智能指针——条款21:巧妙的将对象伪装成指针
- 有效的使用和设计COM智能指针——条款1:智能指针之前世今生
- 有效的使用和设计COM智能指针——条款19:在接口完满的前提下使之最小化。
- 有效的使用和设计COM智能指针——条款2:引用计数的是与非
- 有效的使用和设计COM智能指针——条款10:尽量减少智能指针和接口指针的混用
- 有效的使用和设计COM智能指针——条款26:自动查询接口带来方便同时也潜藏危机
- 有效的使用和设计COM智能指针—条款4:理解ATL的CComPtr提倡简单
- 有效的使用和设计COM智能指针——条款3:按照功能和实现原理选择合适的智能指针
- 有效的使用和设计COM智能指针——条款25:思考兼容取地址操作符带来的若干问题
- 有效的使用和设计COM智能指针——条款4:理解ATL的CComPtr提倡简单,高效
- 有效的使用和设计COM智能指针——条款27:考虑__uuidof与uuid在关键字在不同编译器上的兼容问题
- 有效的使用和设计COM智能指针 ——条款5:了解_com_ptr_t 设计背后的历史原因
- 有效的使用和设计COM智能指针——条款14:有意识的限制智能指针的生命周期
- 有效的使用和设计COM智能指针——条款23:为例外条件准备应对策略。
- 有效的使用和设计COM智能指针——条款15:以原则中的优先级作为取舍的依据