您的位置:首页 > 编程语言 > C语言/C++

有效的使用和设计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();


如果此时spIMyInterface的引用计数恰巧为1,则程序崩溃了,要找出这种问题的所在确实让人抓狂。因此为了线程安全,你可能会在这个运算符的两端加锁,其实大可不必这么大动干戈。只需要将原先智能指针所指向的接口指针稍作保存,问题就可以解决。

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;
}


当然如果考虑到自我赋值中AddRef和Release会有浪费掉几条宝贵的机器指令的话下面这种做法会更加的高效。

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)
{}


这一小节先到此结束吧,之后还会有更多的函数等待我们加入。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐