David's Note: I am not a good writer, and to write a fully new article is a little hard for me. But writing some remark on good articles may be also helpful for others and easier to me. So, from now on, I will try to add a new column “David's Reading” in my blog, and post my thought and idea about good articles I have read.
More Effective C++ Item 附2:一个auto_ptr的实现实例
一个auto_ptr的实现实例
Items M9、M10、E26、E31和E32证明了auto_ptr模板类的非同寻常的作用。不幸的是,目前很少有编译器地提供了一个“正确”的实现(注1)。Items M9和M28大致描述了你怎么自己实现一个,但从事实际项目时有一个更详尽的版本就太好了。
下面是两个auot_ptr的实现。第一个版本文档化了类的接口并在类的定义体外面实现了所有的成员函数。第二个版本将所有的成员函数都实现在定义体内了。在文体上,第二个实现不如第一个,因为它没有将类的接口从实现中分离出来。但auto_ptr只是一个简单的类,所以第二个实现比第一个清晰得多。
这是有专门接口申明的auto_ptr模板:
template<class T>
class auto_ptr {
public:
explicit auto_ptr(T *p = 0); // Item M5 有“explicitfor”
// 的描述
template<class U> // 拷贝构造函数成员模板
auto_ptr(auto_ptr<U>& rhs); // (见Item M28):
// 用另一个类型兼容的
// auto_ptr对象
// 初始化一个新的auto_ptr对象
~auto_ptr();
template<class U> // 赋值操作成员模板
auto_ptr<T>& // (见Item M28):
operator=(auto_ptr<U>& rhs); // 用另一个类型兼容的
// auto_ptr对象给它赋值
T& operator*() const; // 见Item M28
T* operator->() const; // 见Item M28
T* get() const; // 返回包容指针的
// 当前值
T* release(); // 放弃包容指针的
// 所有权,
// 并返回其当前值
void reset(T *p = 0); // 删除包容指针,
// 获得指针p的所有权
private:
T *pointee;
// 大卫注: 为什么要这样? 不解, 在该实现中根本就没有用到auto_ptr<U>的私有成员
template<class U> // 让所有的auto_ptr类
friend class auto_ptr<U>; // 成为友元
};
// 大卫注: 以下是模板类auto_ptr成员函数实现
template<class T>
inline auto_ptr<T>::auto_ptr(T *p)
: pointee(p)
{}
template<class T>
inline auto_ptr<T>::auto_ptr(auto_ptr<U>& rhs)
: pointee(rhs.release())
{}
template<class T>
inline auto_ptr<T>::~auto_ptr()
{ delete pointee; }
// 大卫注: faint, 还可以这样! 模板里面套模板。
template<class T>
template<class U>
inline auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<U>& rhs)
{
if (this != &rhs) reset(rhs.release());
return *this;
}
// 大卫注: 此处需要注意的是assign operation将release形参auto_ptr<U>对象, 上面的拷贝构造也是如此, 以免出现多次delete一个受托管对象的错误, 这也说明我们在传递auto_ptr对象时最好不要传对象, 而要传引用(当然, 你还可以传指针, 不过, 在可以用引用的时候请不要用指针, 因为引用更高效而且更简洁), 否则, 你可能会为程序的行为感到莫名其妙, 受托管的对象可能在你没有意识到的时候被delete了, 这一问题对于基于Reference Count的smart pointer则可能不存在。下面的程序可以说明这一点
/*#include <iostream>
#include <memory>
#include <string>
using namespace std;
void f(auto_ptr<string> apString) // copy-constructor is called
{
cout << "in f()" << endl;
}
int main(int argc, char** argv)
{
auto_ptr<string> apString(new string("abc"));
f(apString);
cout << *apString << endl; // Error! The string object has been deleted by f()!
// To solve this problem, you should use pass apString by reference
auto_ptr<string> apString2;
apString2 = apString; // assign-operator is called
cout << *apString << endl; // Error too!
// To solve this problem, don't use copy constructor of auto_ptr
// the following method is also wrong:
// auto_ptr<string> apString2(*apString);
return 0;
}*/
// 大卫注: 根据以上分析, 我认为auto_ptr的实现应该禁用auto_ptr的copy constructor和assignment, 它使得对象的释放工作变得十分混乱
template<class T>
inline T& auto_ptr<T>::operator*() const
{ return *pointee; }
template<class T>
inline T* auto_ptr<T>::operator->() const
{ return pointee; }
template<class T>
inline T* auto_ptr<T>::get() const
{ return pointee; }
template<class T>
inline T* auto_ptr<T>::release()
{
T *oldPointee = pointee;
pointee = 0;
return oldPointee;
}
template<class T>
inline void auto_ptr<T>::reset(T *p)
{
if (pointee != p) {
delete pointee;
pointee = p;
}
}
以下是所有函数定义在类定义体内的auto_ptr模板。
// 大卫注: 以下代码与上面完全等价,只是将函数实现放在了模板内. jjhou的<STL源码剖析>Ch3借用了以下代码, 但是有删减, 让人有点莫名其妙, 如有不解, 可以参照此处代码。
template<class T>
class auto_ptr {
public:
explicit auto_ptr(T *p = 0): pointee(p) {}
template<class U>
auto_ptr(auto_ptr<U>& rhs): pointee(rhs.release()) {}
~auto_ptr() { delete pointee; }
template<class U>
auto_ptr<T>& operator=(auto_ptr<U>& rhs)
{
if (this != &rhs) reset(rhs.release());
return *this;
}
T& operator*() const { return *pointee; }
T* operator->() const { return pointee; }
T* get() const { return pointee; }
T* release()
{
T *oldPointee = pointee;
pointee = 0;
return oldPointee;
}
void reset(T *p = 0)
{
if (pointee != p) {
delete pointee;
pointee = p;
}
}
private:
T *pointee;
template<class U> friend class auto_ptr<U>;
};
如果你所用的编译器还不支持“explicit”,可以安全地用#define取消它的存在:
#define explicit
这不会造成auto_ptr的任何功能减弱,但导致轻微的安全性减弱。详见Item M5。
如果你的编译器不支持成员模板,你可以使用非模板的auto_ptr拷贝构造函数和赋值操作(描述在Item M28)。这将造成你的auto_ptr在使用上有些小小的不方便,但没有其它方法能模仿成员模板的行为,唉!如果成员模板(或语言中的其它一些特性)对你非常重要,告诉你的编译器提供商。越多的用户要求新的语言特性,提供商将越快地实现它们。
* 注1:
这主要是因为auto_ptr的标准长年来一直没有确定。其最终描述被采纳于1997年11月。其细节可参考本书的主页。注意,此处的auto_ptr版本在实现上比正式版本在具体细节上有小小的省略:实际上auto_ptr位于名字空间std中(见Item M35)并且其成员函数承诺不抛任何异常。
// 大卫注: 原文到此结束, 以下内容由本人添加, 不再特别注明。
根据以上的分析, 上述auto_ptr实现实在有太多不尽人意的地方, 那么颇受好评的SGI STL又是怎么做的呢?
以下内容取自STLport: _auto_ptr.h
#ifndef _STLP_AUTO_PTR_H
# define _STLP_AUTO_PTR_H
_STLP_BEGIN_NAMESPACE
// 加个基类在这里, 有什么特殊用途呢?
class __ptr_base {
public:
void* _M_p;
void __set(const void* p) { _M_p = __CONST_CAST(void*,p); }
void __set(void* p) { _M_p = p; }
};
// 这个类有什么特殊作用吗?
template <class _Tp> class auto_ptr_ref {
public:
__ptr_base& _M_r;
_Tp* const _M_p;
auto_ptr_ref(__ptr_base& __r, _Tp* __p) : _M_r(__r), _M_p(__p) { }
_Tp* release() const { _M_r.__set((void*)0); return _M_p; }
};
template<class _Tp> class auto_ptr : public __ptr_base {
public:
typedef _Tp element_type;
typedef auto_ptr<_Tp> _Self;
_Tp* release() {
_Tp* __px = this->get();
this->_M_p = 0;
return __px;
}
void reset(_Tp* __px=0) {
_Tp* __pt = this->get();
if (__px != __pt)
delete __pt;
this->__set(__px);
}
_Tp* get() const { return __REINTERPRET_CAST(_Tp*,__CONST_CAST(void*,_M_p)); }
# if !defined (_STLP_NO_ARROW_OPERATOR)
_Tp* operator->() const {
_STLP_VERBOSE_ASSERT(get()!=0, _StlMsg_AUTO_PTR_NULL)
return get();
}
# endif
_Tp& operator*() const {
_STLP_VERBOSE_ASSERT(get()!=0, _StlMsg_AUTO_PTR_NULL)
return *get();
}
auto_ptr() { this->_M_p = 0; }
explicit auto_ptr(_Tp* __px) { this->__set(__px); }
#if defined (_STLP_MEMBER_TEMPLATES)
# if !defined (_STLP_NO_TEMPLATE_CONVERSIONS)
template<class _Tp1> auto_ptr(auto_ptr<_Tp1>& __r) {
_Tp* __conversionCheck = __r.release();
this->__set(__conversionCheck);
}
# endif
template<class _Tp1> auto_ptr<_Tp>& operator=(auto_ptr<_Tp1>& __r) {
_Tp* __conversionCheck = __r.release();
reset(__conversionCheck);
return *this;
}
#endif /* _STLP_MEMBER_TEMPLATES */
auto_ptr(_Self& __r) { this->__set(__r.release()); } // 拷贝构造, 同样释放了原对象, 对于普通的smart pointer这是必须的, 但又是容易引起问题的
_Self& operator=(_Self& __r) { // assign operator, 也release了原对象
reset(__r.release());
return *this;
}
~auto_ptr() { /* boris : reset(0) might be better */ delete this->get(); }
auto_ptr(auto_ptr_ref<_Tp> __r) {
this->__set(__r.release());
}
_Self& operator=(auto_ptr_ref<_Tp> __r) {
reset(__r.release());
return *this;
}
# if defined(_STLP_MEMBER_TEMPLATES) && !defined(_STLP_NO_TEMPLATE_CONVERSIONS)
template<class _Tp1> operator auto_ptr_ref<_Tp1>() {
return auto_ptr_ref<_Tp1>(*this, this->get());
}
template<class _Tp1> operator auto_ptr<_Tp1>() {
return auto_ptr<_Tp1>(release());
}
# else
operator auto_ptr_ref<_Tp>()
{ return auto_ptr_ref<_Tp>(*this, this->get()); }
# endif
};
_STLP_END_NAMESPACE
#endif /* _STLP_AUTO_PTR_H */
SGI STL的auto_ptr实现与上面基本相同, 同样也存在copy constructor和assign operator的问题。
而一般的基于Reference count的auto_ptr又是如何实现的呢, look:
template <class T>
class CComPtr
{
public:
typedef T _PtrClass;
CComPtr()
{
p=NULL;
}
CComPtr(T* lp)
{
if ((p = lp) != NULL)
p->AddRef();
}
CComPtr(const CComPtr<T>& lp) // 拷贝构造, 无需对受托对象进行操作, 仅增加引用计数
{
if ((p = lp.p) != NULL)
p->AddRef();
}
~CComPtr()
{
if (p)
p->Release();
}
void Release()
{
IUnknown* pTemp = p;
if (pTemp)
{
p = NULL;
pTemp->Release();
}
}
operator T*() const
{
return (T*)p;
}
T& operator*() const
{
ATLASSERT(p!=NULL);
return *p;
}
//The assert on operator& usually indicates a bug. If this is really
//what is needed, however, take the address of the p member explicitly.
T** operator&()
{
ATLASSERT(p==NULL);
return &p;
}
_NoAddRefReleaseOnCComPtr<T>* operator->() const
{
ATLASSERT(p!=NULL);
return (_NoAddRefReleaseOnCComPtr<T>*)p;
}
T* operator=(T* lp)
{
return (T*)AtlComPtrAssign((IUnknown**)&p, lp);
}
// 对于CComPtr<T>之间的赋值, 不能使用上面的实现
T* operator=(const CComPtr<T>& lp)
{
return (T*)AtlComPtrAssign((IUnknown**)&p, lp.p);
}
// 以下n函数及其实现略
T* p;
};
其中, 函数AtlComPtrAssign实现如下:
ATLINLINE ATLAPI_(IUnknown*) AtlComPtrAssign(IUnknown** pp, IUnknown* lp)
{
if (lp != NULL)
lp->AddRef(); // 增加右操作数的Reference Count
if (*pp)
(*pp)->Release(); // 减小左操作数的Reference Count
*pp = lp; // 给左操作数重新赋值
return lp;
}
从上面可以看出, 基于引用计数的smart pointer实现没有了先前的auto_ptr存在的copy constructor和assign operator的问题。
当然, 以上的讨论并非说我们必须使用基于引用计数的smart pointer, 引用计数毕竟是一种额外的开销, 而且由于需要考虑线程同步, 需要付出更多额外的开销。所以, 最好的解决办法是根据需要进行选择, 在使用auto_ptr时尽量避免传值。但作为一名C++程序员, 将自己分配的对象交给auto_ptr去管理, 你真的放心吗? 反正我是不大用auto_ptr的(诸位能给出一个十分需要auto_ptr, 或应用auto_ptr可以显著优化程序的例子吗?)。
附注:
1. 写完此文, 偶然发现More Effective C++ M28已经给出了与上面类似的结论, 但该文既已写就, 不忍弃之, 故仍然公布于此.
2. 对于文中我存在疑问的地方, 大家有何见解呢?
3. 欢迎批评指正.