之前文章提到写时复制(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<class T>
15 inline Handle<T>::Handle(T* p):ptr(p) //u 默认构造函数
16 {}
17
18 template<class T>
19 inline Handle<T>::Handle(const Handle& rhs):u(rhs.u), ptr(rhs.ptr)//u 拷贝构造函数会将引用计数+1
20 {}
21
22 template<class T>
23 inline Handle<T>& Handle<T>::operator=(const Handle& rhs)
24 {
25 if(u.reattach())//是否仍有句柄绑定到此对象
26 delete ptr;
27 ptr = rhs.ptr;
28 return *this;
29 }
30
31 template<class T>
32 inline Handle<T>::~Handle()//同构造函数 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类型指针是分开的,而如果将这两个成员封装在一起的话呢?
未完待续……