条款21:巧妙的将对象伪装成指针
我们来探讨一下如何让我的智能指针看上去更像是一个“指针”而不是一个“对象”。在这之前,我们来看看C/C++中指针的某些特性,以便我们更加精确的模拟出指针的行为。
指针是C/C++中一个有趣的产物。从根本上讲他就是一个变量,变量能容纳的整数值的取值范围一般为某种平台上一个进程的最大地址空间。这个数值存放着进程地址空间中的具体值对象所在的地址。当我们使用一个指针时,可以进行如下这些操作:
MyObj *p_Obj = &m_Obj; //一个MyObje类型的指针,它内部存放的是m_Obj的地址
DoSomething(*p_Obj ); //取值操作符,返回其指向的具体对象
p_nInr->DoSomething(); //通过指针操作符访问其成员
DoSomethingViaPtr(p_Obj ); //直接传递地址
以上这些知识貌似显得太过于基础,似乎在任何一本C/C++入门书上都可以找到这样的例子。而C++的运算符重载真是好东西,你迫不及待的加入了一下功能:
template <class T>
class CMyComSmartPtr
{
public:
//简简单单,他完成任务了?
//试着找找,看你能发现几处错误?
T operator*()
{
return *p;
}
operator T*()
{
return p;
}
T** operator&()
{
return &p;
}
T* operator->()
{
return p;
}
...
private:
T* p;
};
上面代码确实可以执行,但却会给使用带来一些莫名的错误。试着先找找能否一眼看出来?如果你洞察力并非十分强烈,那我只好用下述代码将其暴露出来了:
MyObj *Myfuncion()
{
CMyComSmartPtr<MyObj> p_Obj = &m_Obj;
(*p_Obj).ChangeValue(); //[1]哦~ 这个操作可不一定会改变m_Obj的值哦~
const CMyComSmartPtr<MyObj> p_cObj = &m_cObj; //m_cObj对象有const修饰
p_cObj->DoConstFunc(); //[2]这句将无法编译通过,因为->不是常成员。
(*p_cObj).DoConstFunc(); //同样无法编译
return p_Obj; //[3]这里或许会抛出异常,但普通指针却绝对不会。
}
哦~ 看来这套智能指针背后丑陋的鬼脸已经暴露无遗了。我们来总结一下他的错误之处处:
错误[1]:它没有区分值传递和引用传递,而在取值操作符上简单的返回了一个T对象。程序会在栈上产生一个临时的拷贝。然后你对其进行了一些修改,这些修改都是针对与这个临时对象的。执行完这句,临时对象将会销毁。然后你所做的改变呢? :(
错误[2]:这套智能指针没有考虑到对常对象的支持(const修饰的对象)。如果智能指针是一个常对象,他则无法调用到常对象中的常成员。这是由于->操作符是非常性的。
错误[3]:这套智能指针无法兼容异常安全的代码,他的很多地方不符合C/C++约定。如转换函数不应当抛出异常。当它出现在为异常安全而编写的代码之中时,异常安全会失效。
看到了这些问题之所在,你可能会更加细心的编写这些操作符重载。参考几个成熟的方案,如CComPtr和_com_ptr_t,或许下面这个智能指针的表现会稍微好一点:
template <class T>
class CMyComSmartPtr
{
public:
//考虑到异常安全,引用传递和常性约束的版本。
T& operator*() const //加入常性约束,并返回一个引用
{
return *p;
}
operator T*() const throw() //加入常性约束,并禁止抛出异常
{
return p;
}
T* operator->() const throw()//兼容常性对象,同时也不抛出异常
{
return p;
}
...
private:
T* p;
};
上述改进并非完善,但你或许还会追加几个问题。你或许又在问,为什么operator*() 允许抛出一个以后呢?对此C++标准并不禁止取值操作抛出异常的。因此考虑到你的智能指针所指向的类型确实有可能因为取值操作而抛出异常,我们并没有给起接上一个throw()关键字。试想一下上述智能指针的T类型是一个_com_ptr_t而,这确实有点荒唐,但那样的程序也确实可以执行。_com_ptr_t在operator*操作的时候抛出异常了(它在所指为空的时候会抛出一个异常),那么你的智能指针也应当将异常继续抛出,以供上层处理。
如果你善于观察,可能还会发现一个问题。用const修饰的operator->返回的并非一个const对象指针!!是的。这确实是一个问题。与其说这样设计出来的指针不支持const对象,不如说COM技术本生就不支持const对象。试想一下,一个const对象如何在必要的时候调用AddRef()和Release()。因此果断放弃const对于T类型的修饰永远是明智的。
最后,我们让他更加完善一些。但这需要你有这么一个意识:“尽可能早的暴露程序中的错误”。用断言为程序设防,将不可能出现的错误蹦出来,这样不仅使得你的智能指针更加健壮,而且能避免调用者某些使用上的错误。当他看到程序崩溃,应该会停下来修改自己的代码,并认真的检查逻辑上的问题:
template <class T>
class CMyComSmartPtr
{
public:
T& operator*() const
{
assert(p != NULL);
return *p;
}
operator T*() const throw() //加入常性约束,并禁止抛出异常
{
return p;
}
T* operator->() const throw()//兼容常性对象,同时也不抛出异常
{
assert(p != NULL);
return p;
}
...
private:
T* p;
};
OK~ 这个条款结束吧。