C++设计中的Handle处理类

Handle这个内容是在看Andrew和Barbara夫妇的《C++沉思录》中接触到的,被广为翻译为句柄,用他来控制其所管理的类。刚开始看瞄的时候就觉得:握草,这不就是智能指针的原型么,难道是没有智能指针时代的轮子?但是耐着性子看完后,觉得还是收获不少的,起码的话算是对智能指针中引用计数、写时复制的设计实现写的比较清楚了。

题外话,其实智能指正在我司也早有轮子,并且一直被使用至今了。今天过细看了下代码,发现都是最简单的Scoped非拷贝使用方式,而在需要外传的时候都是使用引用的方式传递出去,所以算是挂羊头卖狗肉吧,Shared类其实根本就没做到Shared的事儿,就是个简单的RAII做的事儿。不过,因为我们的业务逻辑比较简单,所以长久使用起来也没有什么问题……

一、简单实现

1.1 准备工作

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

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可以被拷贝成任意份,显然不成立。所以,单独的设计一个类来维护这个信息:

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就直接释放资源对象了:

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_;
}

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

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成为一个新对象呢?

值语义的访问在修改底层数据的时候需要拷贝一份新的出来供修改,作为写时复制的功能,算是针对可变对象的一种优化技巧,其访问函数为:

Handle& Handle::x(int x) {
  if(up_->u_ != 1) {
    --up_->u_;
    up_ = new UPoint(up_->p_);
  }

  up_->p_.x_(x);
  return *this;
}

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

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

二、句柄类的通用化

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

2.1 基本使用

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

class Handle {
public:
  Handle(int, int);
  Handle(const Point& p);
private:
  Point* p_;  //既可以指向Point,也可以指向其派生类
  int* u_;
};

通过分离引用计数,不在需要辅助的UPoint类,而直接使用成员变量存储指向数据的指针,以及保存引用计数的指针(此处必须是指针而不能是值)。

此时,其构造函数和析构函数为(注意任何情况下p和u都会成对出现):

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_;
  }
}

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

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的析构函数

bool UseCount::only() { return *p_ == 1; }
int UseCount::use_count() { return *p_; }

~Handle::Handle() {
  if(u_.only())
    delete p_;
}

(2) 赋值运算符的实现

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) 写时复制的实现

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;
}

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

本文完!

我来评几句
登录后评论

已发表评论数()