之前文章提到写时复制(copy-on-write)技术,要实现这种功能,针对上文中Handle代码,需要将size_t * use这个抽象出来,封装成一个引用计数类,提供写时复制功能。CUseCount类实现如下:
1 class CUseCount 2 { 3 public: 4 CUseCount(); 5 CUseCount(const CUseCount&); 6 ~CUseCount(); 7 8 bool only()const; //判断引用计数是否为0, 句柄类无法访问private int*p, 故提供此函数 9 bool reattach(const CUseCount&); //对计数器的操作, 用来代替 operator = 10 11 bool makeonly(); //写时复制, 表示是否需要赋值对象本身 12 13 private: 14 CUseCount& operator=(const CUseCount&); //提供reattach函数代替 operator = 15 int *p; //实现计数 16 }; 17 18 CUseCount::CUseCount():p(new int(1)) 19 {} 20 21 CUseCount::CUseCount(const CUseCount& u):p(u.p) 22 { 23 ++*p; 24 } 25 26 CUseCount::~CUseCount() 27 { 28 if(--*p == 0) 29 delete p; 30 p = 0; 31 } 32 33 bool CUseCount::only()const 34 { 35 return *p == 1; 36 } 37 38 bool CUseCount::reattach(const CUseCount& u) 39 { 40 ++*u.p; //避免 this == &u, 先对 *u.p 加一 41 if(--*p == 0) //如果引用计数值为0删除 this->p, 重新绑定p 42 { 43 delete p; 44 p = u.p; 45 return true;//返回true表示句柄此时绑定对象引用数为0, 可删除 46 } 47 p=u.p; //如果引用计数值不为0,只是重新绑定p 48 return false; //返回false表示此时仍有句柄绑定到此对象,不可删除 49 } 50 51 bool CUseCount::makeonly() 52 { 53 if(*p == 1) //确保句柄唯一, 则不需要进行复制 54 return false; 55 //其他情况则必须复制 56 --*p; 57 p = new int(1); 58 return true; 59 }
修改之前Handle代码,在其中定义CUseCount对象:
1 template<class T> class Handle 2 { 3 public: 4 Handle(T *p = 0); 5 Handle(const Handle& h); 6 Handle& operator=(const Handle&); 7 ~Handle(); 8 //other member functions 9 private: 10 T* ptr; 11 CUseCount u; //将引用计数类抽象 12 }; 13 14 template<typename T> 15 inline CHandler<T>::CHandler(T* p):ptr(p) //u 默认构造函数 16 {} 17 18 template<typename T> 19 inline CHandler<T>::CHandler(const CHandler& rhs):u(rhs.u), ptr(rhs.ptr)//u 拷贝构造函数会将引用计数+1 20 {} 21 22 template<typename T> 23 inline CHandler<T>& CHandler<T>::operator=(const CHandler& rhs) 24 { 25 if(u.reattach())//是否仍有句柄绑定到此对象 26 delete ptr; 27 ptr = rhs.ptr; 28 return *this; 29 } 30 31 template<typename T> 32 inline CHandler<T>::~CHandler()//同构造函数 u 的析构函数被调用 33 { 34 if(u.only()) //引用计数只有唯一一个对象时,则进行delete操作 35 delete ptr; 36 }
使用引用计数封装后的Handle类运行前一篇中的例子时,仍然会出现*hp随着*hp2重新赋值而改变的情况,此版本的句柄类(即Handle模板类)情况下要实现copy-on-write,目前我能想到的有两种方式:
1 重载句柄类的 operator-> 和 operator* :
1 template<class T> 2 inline T& Handle<T>::operator*() 3 { 4 if(u.makeonly()) 5 delete ptr; 6 ptr = new T(*ptr); 7 8 if(ptr) return *ptr; 9 throw std::runtime_error 10 ("dereference of unbound Handle"); 11 } 12 13 template<class T> 14 inline T* Handle<T>::operator->() 15 { 16 if(u.makeonly()) 17 delete ptr; 18 ptr = new T(*ptr); 19 20 if(ptr) return ptr; 21 throw std::runtime_error 22 ("access through of unbound Handle"); 23 }
以这种方式实现的写时复制,基本上所有操作都会调用原生指针的值拷贝(拷贝构造函数),因为除非对象之定义不使用,否则都会调用到operator->和operator*两个函数,而有些操作并没有改变原生指针指向的内容,还是发生了拷贝,很简单的例子:
1 int main() 2 { 3 Handle<int> hp(new int(12)); 4 Handle<int> hp2(hp); 5 cout<<*hp<<" "<<*hp2<<endl; 6 7 return 0; 8 }
代码中对hp2的操作是读操作,但程序依然调用了operator*,进行写时复制,这种情况下是没有必要的,注意到重载的是非const版本的operator->和operator*,而对于const版本的两个函数,默认不进行写时复制,这样就得在编码过程中尽量使用const对象了。
2 写时复制对应具体的操作。不重载operator->和operator*函数,而是具体需要修改T类型成员变量时,进行写时复制,分析下例:
1 int main() 2 { 3 Handle<string> hp(new string("Grubby")); 4 Handle<string> hp2(hp); 5 char c = hp[3]; 6 c = ‘e‘; 7 8 return 0; 9 }
要在具体(写)操作时实现写时复制,即hp调用operator[]时,那么Handle类必须重载operator[],在其中完成复制工作:
1 template<T> 2 char & Handle<T>::operator[](int index) 3 { 4 if(u.makeonly()) 5 ptr = new T(*ptr); 6 return *ptr[index]; 7 }
Handle作为模板类,其中托管的T类型未知,如果针对每个T类型都要试Handle重载一些具体的写操作,那么Handle类会爆炸掉,想到一个方法来避免此种情况,是Handle作为基类,而完成写时复制的类继承自Handle,比如上例中,可以如下修改:
首先修改Handle中的访问标签:
1 template<class T> class Handle 2 { 3 public: 4 Handle(T *p = 0); 5 Handle(const Handle& h); 6 Handle& operator=(const Handle&); 7 ~Handle(); 8 //other member functions 9 protected: //使派生类也可以访问此标签下的成员 10 T* ptr; 11 CUseCount u; //将引用计数类抽象 12 };
定义继承自Handle<string>的派生类:
1 class StrHandle : public Handle<string> 2 { 3 public: 4 char & operator[](int index) //实现写时复制 5 { 6 if(u.makeonly()) 7 ptr = new T(*ptr); 8 return *ptr[index]; 9 } 10 }; 11 12 int main() 13 { 14 StrHandle<string> hp(new string("Grubby")); 15 StrHandle<string> hp2(hp); 16 char c = hp[3]; 17 c = ‘e‘; 18 19 return 0; 20 }
文中提到的两种实现写时复制方法都比较复杂,不是很直观,添加了很多额外的代码,文中实现的句柄类中引用计数和T类型指针是分开的,而如果将这两个成员封装在一起的话呢?
未完待续……