C++设计中的Handle处理类

  Handle这个内容是在看Andrew和Barbara夫妇的《C++沉思录》中接触到的,被广为翻译为句柄,用他来控制其所管理的类。刚开始看瞄的时候就觉得:握草,这不就是智能指针的原型么,难道是没有智能指针时代的轮子?但是耐着性子看完后,觉得还是收获不少的,起码的话算是对智能指针中引用计数、写时复制的设计实现写的比较清楚了。
  题外话,其实智能指正在我司也早有轮子,并且一直被使用至今了。今天过细看了下代码,发现都是最简单的Scoped非拷贝使用方式,而在需要外传的时候都是使用引用的方式传递出去,所以算是挂羊头卖狗肉吧,Shared类其实根本就没做到Shared的事儿,就是个简单的RAII做的事儿。不过,因为我们的业务逻辑比较简单,所以长久使用起来也没有什么问题……

一、简单实现

1.1 准备工作

  作者过于循循善诱的细节东西就不细说了,Point这个类是我们实际的用户类,跟业务相关的我们不管;Handle类是我们要实现的句柄类,我们的目的是要将Point的对象绑定到Handle对象上,让handle控制他所绑定的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point {
public:
Point(int x, int y): x_(x), y_(y) {}
private:
int x_; int y_;
};
class Handle {
public:
Handle(int, int);
Handle(const Point& p);
private:
UPoint* up_; // TODO...
};

  为了用户使用的舒适傻瓜,那么Handle就应该自动接手用户类Point资源的创建和销毁,通过Handle的构造函数我们得知申请资源的时候就有两种方式:(1)直接让Handle类的构造函数参数和Point类构造函数签名一致,然后做一个参数转发;(2)用拷贝的方式,创建一个现有Point的对象的副本,而原始对象的资源我们不关心。

1.2 使用引用计数

  使用句柄的目的除了自动管理对象外,一个目的就是避免不必要的对象复制,允许多个句柄对象绑定到同一个对象上面。然后句柄可以作为参数传入到函数中去,句柄对象可以被复制,但是我们必须清楚的了解到有多个Handle绑定到对象上,以确定什么时候可以释放资源。
  引用计数不能放到上面两个类的任何一个,原因:如果放到Point,那么以为着所有的类都需要重写,侵入性太强了而且使用不便;如果放到Handle,但是Handle可以被拷贝成任意份,显然不成立。所以,单独的设计一个类来维护这个信息:

1
2
3
4
5
6
7
8
9
10
class UPoint{
friend class Handle;
private:
Point p_; //直接对象存储
int u_;
UPoint(): u_(1) {}
UPoint(int x, int y): p_(x, y), u_(1) {}
UPoint(const Point& p): p_(p), u_(1) {}
};

  注意,这里UPoint类的所有成员都是私有的,那么能够确认只有Handle类构造其对象,能够保证内部的成员是初始化良好的。这样,句柄类的构造函数的调用就显而易见了,析构函数发现其引用数量为0就直接释放资源对象了:

1
2
3
4
5
6
7
8
Handle::Handle(): up_(new UPoint()) {}
Handle::Handle(int x, int y): up_(new UPoint(x, y)) {}
Handle::Handle(const Point& p): up_(new UPoint(p)) {}
Handle::~Handle(){
if(--up_->u_ == 0)
delete up_;
}

  复制构造函数最为简单,只需要增加其引用计数就可以了;赋值操作的话,其左侧操作数的句柄会被改写,所以应该递减其引用计数,而且需要特别注意的是自赋值的安全性问题。

1
2
3
4
5
6
7
8
9
Handle::Handle(const Handle& rhs): up_(rhs.up_) { ++up_->u_; }
Handle& Handle::operator=(const Handle& rhs) {
++rhs.up_->u_;
if(--up_.u_ == 0)
delete up_;
up_ = rhs.up_;
return *this;
}

  Handle代理UPoint.Point的操作就不显示了,主要是一个操作转发(或者warpper)。

1.3 写时拷贝

  我们看见,在UPoint中的Point是存储的直接对象。这里引申出的一个概念就是Point对象可以表现出值语义和引用/指针语义,因为一旦我们通过Handle修改底层Point对象的时候,这种语义差别就体现出来了。
  他们实现的差异就是当调用非const成员函数修改底层对象的时候,是否是直接修改,还是拷贝出一个新的UPoint对象并将其引用计数置为1成为一个新对象呢?
  值语义的访问在修改底层数据的时候需要拷贝一份新的出来供修改,作为写时复制的功能,算是针对可变对象的一种优化技巧,其访问函数为:

1
2
3
4
5
6
7
8
9
Handle& Handle::x(int x) {
if(up_->u_ != 1) {
--up_->u_;
up_ = new UPoint(up_->p_);
}
up_->p_.x_(x);
return *this;
}

  指针/引用语义则相对简单的多:

1
2
3
4
Handle& Handle::x(int x) {
up_->p_.x_(x);
return *this;
}

二、句柄类的通用化

  上面的实现方法,其语义和功能是完整的,但是缺点也十分明显:为了将句柄捆绑到类T(Point)上,必须定义一个具有类型为T的成员的新类(UPoint),这不但使得句柄类使用复杂,而且对于派生类使用该句柄也没有任何的好处(直接定义的类对象)。

2.1 基本使用

  另外一种好的方法,是将引用计数对象单独开来作为单独的对象,而不是直接向类对象本身上试图捆绑什么东西或者创建包装类。

1
2
3
4
5
6
7
8
class Handle {
public:
Handle(int, int);
Handle(const Point& p);
private:
Point* p_; //既可以指向Point,也可以指向其派生类
int* u_;
};

  通过分离引用计数,不在需要辅助的UPoint类,而直接使用成员变量存储指向数据的指针,以及保存引用计数的指针(此处必须是指针而不能是值)。
  此时,其构造函数和析构函数为(注意任何情况下p和u都会成对出现):

1
2
3
4
5
6
7
8
9
Handle::Handle(): p_(new Point()), u_(new int(1)) {}
Handle::Handle(int x, int y): p_(new Point(x, y)), u_(new int(1)) {}
Handle::Handle(const Point& p): p_(new Point(p)), u_(new int(1)) {}
Handle::~Handle(){
if(--*u_ == 0){
delete u_; delete p_;
}
}

  而其拷贝构造函数和拷贝赋值运算符也显而易见:

1
2
3
4
5
6
7
8
9
10
11
Handle::Handle(const Handle& rhs): p_(rhs.p_), u_(rhs.u_) { ++*u_; }
Handle& Handle::operator=(const Handle& rhs) {
++*u_;
if(--*u_ == 0) {
delete p_; delete u_;
}
p_ = rhs.p_;
u_ = rhs.u_;
return *this;
}

2.2 抽象出UseCount类

  上面的类的功能也是完整的,不过直接加入一个计数器就需要在Handle中做出很多计数器语义的判断,所以的话,抽象出一个计数器UseCount类,实现计数器专职的功能会让这个模型更加的优雅。
  其普通内容就不摘抄罗列了,主要是引用计数的接口和实现比较容易出错,记录下来。
  (1) 实现Handle的析构函数

1
2
3
4
5
6
7
bool UseCount::only() { return *p_ == 1; }
int UseCount::use_count() { return *p_; }
~Handle::Handle() {
if(u_.only())
delete p_;
}

  (2) 赋值运算符的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool UseCount::reattach(const UseCount& rhs) {
++rhs.p_;
if(--*p_ == 0) {
delete p_;
p_ = rhs.p_;
return true;
}
p_ = rhs.p_;
return false;
}
Handle& Handle::operator= (const Handle& rhs) {
if(u.reattach(rhs.u_)) // 已经增加引用计数了
delete p_;
p_ = rhs.p_;
return *this;
}

  (3) 写时复制的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool UseCount::makeonly() {
if(only())
return false;
--*p_;
p_ = new int(1);
return true;
}
Handle& Handle::x(int x0){
if(u_.makeonly())
p_ = new Point(*p_); // 拷贝一份
p_->x(x0);
return *this;
}

  小结:算是对引用计数和智能指针的原理有所了解了,而且运用模板和参数完美转发,实现个真正智能指针的轮子也是可以的哦!

本文完!

参考