句柄-摘自《C++沉思录》Andrew Koenig

第一部分

       代理类能让我们在一个容器中存储类型不同但相互关联的对象。这种方法需要为每个对象创建一个代理,并要将代理存储在容器中。创建代理将会复制所代理的对象,就像复制代理一样。

       但是,如果想避免这些复制该怎么做呢?本章将考察另一个通常叫做句柄(hande)的类。它允许在保持代理的多态行为的同时,还可以避免不必要的复制。

1.   问题

       对于某些类来说,能够避免复制其对象是很有好处的。有可能对象会很大,复制起来消耗太大。也有可能每个对象代表一种不能轻易被复制到资源,譬如文件。还有可能某些其他的数据结构已经存储了对象的地址,把副本的地址插入到那些数据结构中代价会非常大,或者根本不可能。还有可能这个对象代表着位于网络连接另一端的其他对象。或者,我们可能处于一个多态性的环境中,这个环境中,我们能够知道对象的基类的类型,但是不知道对象本身的类型或者怎样复制这种类型的对象。

       另一方面,通常由于参数和返回值是通过复制自动传递的,使用C++函数可以很轻易地进行复制操作。用引用传递参数可以避免对它们的复制,但是必须记住,这样做,无论如何对返回值来说都不是那么容易。

       同样,也可以避免使用指针来复制对象,实际上,指针(或者引用)对于C++的多态性来说是十分必要的。但是这样的话,我们就必须牢牢记住要编写接受指针的函数,而不是接受对象的函数。而且,使用对象指针比直接使用对象要困难。

       未初始化的指针是非常危险的,但是也没有什么简单的办法可以防范。在某些C++实现中,只要对这样的指针实施复制操作就会导致彻底崩溃:

void f()
{
	int* p;			// 没有初始化
	int* q = p;		// 未定义操作
}

       在这样的实现中,管理内存的硬件总是要检查每个被复制到指针,看它们是不是真的指向了程序所分配的内存位置上。因此,复制p会导致硬件陷阱。

       另外,无论何时,只要有几个指针指向同一个对象,就必须考虑要在什么时候删除这个对象。如果删除得太早,就会有某个仍然指向它的指针存在着。再使用这个指针就会发生未定义行为。如果删除对象太晚,又会占用本来早就可以另作它用的内存空间。事实上,如果等得太久,就可能失去最后一个指向该对象的指针,以至于最终无法释放这个对象了。

       需要一种方法,让我们在避免某些缺点(如缺乏安全性)的同时能够获取指针的某些优点,尤其是能够在保持多态性的前提下避免复制对象的代价。C++的解决方法就是定义一个适当的类。由于这些类的对象通常被绑定到它们所控制的类的对象上,所以这些类常被称为handle类 (handle classe)。因为这些 handle 的行为类似指针,所以人们有时也叫他们智能指针(smart pointer)。然而,handle 毕竟和指针像差太远,因此只能在极其有限的情况下才能把两者视为相同。

2.   一个简单的类

       我们假定一个包含两个整数的类。这样的类的用途是什么呢?选择最容易想到的就是位图上某点的坐标。所以将它称为 Point 类。假设该类能够支持许多操作,下面就随便列出一组:

class Point{
public:
	Point(): xval(0), yval(0) {}
	Point(int x, int y): xval(x), yval(y) {}
	int x() const { return xval; }
	int y() const { return yval; }
	Point& x(int xv) { xval = xv; return *this; }
	Point& y(int yv) { yval = yv; return *this; }
private:
	int xval, yval;
};

       有一个无参数的构造函数,用于在没有明确提供坐标的情况下创建一个 Point。这是很重要的,譬如对于要创建一个 Point 的数组来说。我们可能只采用了一个带缺省参数的构造函数,而不是另外两个构造函数:

Point(int x = 0, int y = 0): xval(x), yval(y) {}

       那么,它就应该可以只用一个参数(另一个参数缺省为零)来构造一个 Point,而这样做几乎可以肯定是错的。

       默认情况下,由编译器生成的赋值操作符和复制构造函数会按照我们希望的那样运行,因此,不必显式地定义它们。没有指针成员的函数常常出现这种情况。

       当然,我们需要一种能够访问到我们的 Point 对象的元素的途径。我们可能还需要有办法来改变这些元素--有多少编写类的人就有多少种不同的改变类元素的方法。这个例子中用到的技术重载了 x 和 y 成员函数,举例来说,如果 p 是一个指针,则

int x = p.x();

将 p 的 x 坐标复制到 x 中,而

p.x(42);

将 p 的 x 坐标设为 42。这些会引起变化的操作返回对它们的对象的引用,即 p.x(42).y(24) 将设置 p 的 x 坐标为 42,设置 p 的 y 坐标为 24。 

3.   绑定到句柄

       从 Point 对象初始化 handle 应该完成些什么任务?

       Point p;

       Handle h(p);            // 这应该是什么含义?

       浅显地说,我们可能希望上面这段代码将句柄直接绑定到对象 p 上,但是稍加思考我们就会知道这是不会起作用的。问题在于对象 p 是直接处于用户的控制之下的。一旦用户删除掉 p, handle 又会怎样?显然,删除 p 后应该使 handle 无效,但是 handle 如何才能知道 p 被删除了呢?同样,应该同时也删除 handle 吗? 若删除,则如果 p 是一个局部变量,那么 p 就会被删除两次: handle 离开时一次, p 自己超出作用域时一次。如果说

       Handle h(p);

将 handle 直接绑定到了对象 p 上,则我们的 handl 最好与 p 的内存分配和释放无关。如果这样做是可行的,那么我们也就可以使用指针或者引用,而根本不必再创建一个单独的类。

       handle 应该“控制”它所绑定到的对象,把这种似乎很有道理的建议反过来,也就是 handle 应该创建和销毁对象。这样,就有两种可能的选择:可以创建自己的 Point 对象并把它赋给一个 handle 去进行复制,或者可以把用于创建 Point 的参数传给这个 handle。我们要允许这两种方法,所以想让 handle 类的构造函数和 Point 类的构造函数一样。换言之,我们想用

       Handle h0(123, 456);

来创建绑定到新分配的坐标为 123 和 456 的 Point 的 handle,而用

       Handle h(p);

创建一个 p 的副本,并将 handle 绑定到该副本。这样,handle 就可以控制对副本的操作。从效果上说,handle 就是一种只包含单个对象的容器。

4.   获取对象

       假设我们有一个绑定到 Point 对象的 handle,应当如何去访问这个 Point 呢?要是一个 handle 在行为上类似一个指针,则可以使用 operator-> 将 handle 的所有操作转发给相应的 Point 操作来执行:

class Handle{
public:
	Point* operator->();
	//...
};

       其实这个简单的方法基本上就差不多了--可惜这种方法所得到的 handle 跟指针实在太相似了。把所有的 Point 操作都通过 operator->转发了,没有简单的办法禁止一些操作,也没法有选择地改写一些操作。

       例如,如果我们希望 handle 能够对对象的分配和回收拥有完全的控制权,则最好能够阻止用户直接获得那些对象的实际地址。因为这样一来就过多地暴露了内存分配方面的策略,将来一旦想要改变分配的策略就麻烦多多。但是如果我们的 handle 类有 operator->操作,则可以像下面这样直接调用 operator->:

       Point* addr = h.operator->();

从而获得底层 Point 对象的地址。

       所以,如果想要把真实地址隐藏起来,就必须避免使用 operator->,而且必须明确地选择让我们的 handle 类支持哪些 Point 操作。

5.   简单的实现

       现在我们已经了解了足够的东西,可以开始实现自己的 handle 类了。如果希望绕开 operator->(),就必须为 Handle 提供自己的 x 和 x 操作,显然两者的返回值要么是 int (如果以无参数形式调用),要么是 Handle& (如果以单参形式调用)。我们得给这个类一个缺省的构造函数,理由跟给 Point 一个缺省构造函数一样:为了允许数组和其他容器容纳 Handle。确定了 x 和 x 操作之后,我们已经可以给出 Handle 类的大致轮廓了:

class Handle{
public:
	Handle();
	Handle(int, int);
	Handle(const Point&);
	Handle(const Handle*);
	Handle operator=(const Handle&);
	~Handle();

	int x() const;
	Handle& x(int);
	int y() const;
	Handle& y(int);

private:
	//...
};

       剩下需要做的事情就是定义 private 数据和成员函数,而这些地方也正是可以实现语义变化的地方。

5.   引用计数型句柄

       之所以要使用句柄,原因之一就是为了避免不必要的对象复制。也就是说,得允许多个句柄绑定到单个对象,否则很难想象如何把一个句柄作为参数传入函数,因为那要求复制句柄而不复制对象。我们必须了解有多少个句柄绑定在同一个对象上,只有这样才能确定应当在何时删除对象。而通常使用引用计数(use count)来达到这个目的。

       当然,这个引用计数不能是句柄的一部分。如果这么干,那么每一个句柄都必须知道跟它一起被绑定到同一个对象的其他所有句柄的位置,唯有如此才能去更新其他句柄的引用计数数据。同时,也不能让引用计数成为对象的一部分,因为那样要求我们重写已经存在的对象类。因此,我们必须定义一个新的类来容纳一个引用计数和一个 Point 对象。我们称之为 UPoint。这个类纯粹是为了实现而设计的,所以我们把其所有成员都设置为 private,并且将我们的句柄类声明为友元。我们生成一个 UPoint 对象时,其引用计数始终为 1,因为该对象的地址会被马上存储在一个 Handle 对象中(由于UPoint全部成员都是 private 的,所以如果有一个 UPoint 对象被生成,则必然是应其友元类 Handle 之要求而为之,因此可以肯定,该UPoint地址将会马上被某Handle对象所保存。--译者注)。而在另一方面,我们希望能够以创建 Point 的全部方式创建 UPoint 对象,所有我们把 Point 的构造函数照搬过来:

class UPoint{
	//所有成员都是私有的
	friend class Handle;
	Point p;
	int u;

	UPoint(): u(1) {}
	UPoint(int x, int y): p(x, y), u(1) {}
	UPoint(const Point& p1): p(p0), u(1) {}
};

       除了这些操作之外,我们将通过直接引用 UPoint 对象成员的方式操作 UPoint 对象。

       现在可以回到 Handle 类,继续完成下面的工作细节:

class Handle{
public:
	//和以前一样
	Handle();
	Handle(int, int);
	Handle(const Point&);
	Handle(const Handle*);
	Handle operator=(const Handle&);
	~Handle();
	int x() const;
	Handle& x(int);
	int y() const;
	Handle& y(int);

private:
	//添加的
	UPoint* up;
};

       大部分构造函数都很简单--以合适的参数分配一个UPoint:

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)) {}

       析构函数也很简单--它递减引用计数,如果发现引用计数值达到了 0,就删除 UPoint对象。

Handle::~Handle()
{
	if(--p->u == 0)
		delete up;
}

       连复制构造函数也是简单直接的,记着,因为有一个引用计数,所以避免了复制 Point:

Handle::Handle(const Handle& h): up(h.up) { ++up->u; }

       这里所需要做的就是将被“复制”对象的引用计数增 1,这样原先的句柄和其副本都指向相同的 UPoint 对象。

       赋值操作符稍微复杂一点,因为左侧句柄所指向的目标将会被改写,所以必须将左侧句柄所指向的 UPoint 对象的引用计数减 1.这种做法反映了一个事实:左侧的句柄在赋值后将指向另一个对象。然而,当我们将引用计数减 1 时必须注意,这一操作即使在左右两个句柄引用同一个 UPoint 对象时也能正确工作。

       为了确保这一点,最简单的办法就是首先递增右侧句柄指向对象的引用计数,然后再递减左侧句柄所指向对象的引用计数:

Handle& Handle::operator=(const Handle& h)
{
	++h.up->u;
	if(--up->u == 0)
		delete up;
	up = h.up;
	return *this;
}

       存取函数也是简单明了的:

int Handle::x() const { return up->p.x(); }
int Handle::y() const { return up->p.y(); }

       然而,当我们来考虑改动性的函数时,事情立刻就变得有趣了。原因是,这里必须作出决定,到底我们的句柄需要值语义还是之指针语义。

7.   写时复制

       从实现的角度看,我们将 Handle 类设计成“无需对 Point 对象进行复制”的形式,可关键问题是是否希望句柄类在用户面前的行为也是这样。例如:

Handle h(3, 4);
Handle h2 = h;			// 复制 Handle
h2.x(5);			// 修改 Point
int n = h.x();			// 3 还是 5?

       如果希望句柄为值语义,则在这个例子里,我们希望 n 等于 3,因为既然将 h 复制到了 h2,那么再改变 h2 的内容就不应该影响 h 的值了。可另一方面,可能希望 handle 表现得像指针或者引用,也就是说 h 跟 h2 绑定到同一个对象上,改变一个就影响到另一个。两种方式都可能,但是我们必须作出选择。

       如果采用指针语义,则永远不必复制 UPoint 对象,这样函数改动也就十分微不足道了:

Handle& Handle::x(int x0)
{
	up->p.x(x0);
	return *this;
}

Handle& Handle::y(int y0)
{
	up->p.y(y0);
	return *this;
}

       然而,如果需要值语义,就必须保证所改动的那个 UPoint 对象不能同时被任何其他的 Handle 所引用。这倒不难,只要看看引用计数即可。如果是 1,则说明 Handle 是唯一一个使用该 UPoint 对象的句柄;其他情况下,就必须复制 UPoint 对象,使得引用计数变成 1:

Handle& Handle::x(int x0)
{
	if(up->u != 1){
		--up->u;
		up = new UPoint(up->p);
	}
	up->p.x(x0);
	return *this;
}

       Handle::y 与此类似。重写 Handle::x 之后我们会发现

	if(up->u != 1){
		--up->u;
		up = new UPoint(up->p);
	}

需要在每一个改变 UPoint 对象的成员函数中重复,这暗示了我们应当设计一个 private 成员函数,以保证我们的 Handle 引用计数为 1。

       这一技术通常称为 copy on write (写时复制),其优点是只有在绝对必要时才进行复制,从而避免了不必要的复制,而且额外开销也只有一丁点儿。在涉及到句柄的类库中,这一技术经常用到。

第二部分    

       第一部分谈及了一种向类中添加句柄和引用计数的技术,以能够通过只控制引用计数就能够高效地“复制”该类的对象。这种技术有一个明显的缺点:为了把句柄捆绑到类 T 的对象上,必须定义一个具有类型为 T 的成员的新类。这个要求会使事情变得困难起来,当要捆绑这样的句柄到一个继承自 T 的(静态的)未知类的对象时,这个缺点就变得明显了。

       还有一种定义句柄类的方法可以袮补这个缺点。简单的说,主要思想就是将引用计数从数据中分离出来,把引用计数放入它自己的对象中,不是如下图中的两个对象:

 

而是有 3 个对象,如下图所示:

 

        第一眼看上去是很难看出为什么用 3 个数据结构取代两个就会起到作用。然而,结果显示这样做会增加了模块化的程度而没有增加额外的复杂性。这是由于没有必要直接往类对象本身上绑定什么东西了。

1.   回顾

        假如我们有一个表示位图显示上某点坐标的类。这个类可能是这样的:

class Point{
public:
	Point(): xval(0), yval(0) {}
	Point(int x, int y): xval(x), yval(y) {}
	int x() const { return xval; }
	int y() const { return yval; }
	Point& x(int xv) { xval = xv; return *this; }
	Point& y(int yv) { yval = yv; return *this; }
private:
	int xval, yval;
};

       第一部分描述的技术涉及到要创建一个 Point 和一个引用计数的新类:

class UPoint{
	//所有成员都是私有的
	friend class Handle;
	Point p;
	int u;
	UPoint(): u(1) {}
	UPoint(int x, int y): p(x, y), u(1) {}
	UPoint(const Point& p1): p(p0), u(1) {}
};

       接着我们定义了一个包含指向 UPoint 对象的指针的句柄类,以及相关的构造函数、析构函数和存取函数:

class Handle{
public:
	//和以前一样
	Handle();
	Handle(int, int);
	Handle(const Point&);
	Handle(const Handle*);
	Handle operator=(const Handle&);
	~Handle();
	int x() const;
	Handle& x(int);
	int y() const;
	Handle& y(int);
private:
	UPoint* up;
};

2.   分离引用计数

       如果把引用计数从 Point 中分离出来,就会改变对句柄类的实现

class Handle{
public:
	// 和前面一样

private:
	Point* p;
	int* u;			// 指向引用计数的指针
};

       这里,对于 Handle 类的 Public 声明与以前的版本没有区别;用户看不到任何差别。但是,类 UPoint 消失了;不再有指向 UPoint 的指针来,我们用指向 Point 的指针和指向一个 int 的指针表示引用计数。为了简化对引用计数的处理,稍候我们会回过头来定义一个辅助类。

       使用 Point* 而不是 UPoint* 是很重要的,因为正是 Point* 使我们不仅能够将一个 Handle  绑定到一个 Point,还能将其绑定到一个继承自 Point 的类的对象。

       修改了我们的实现中的数据结构部分之后,让我们来看看能否实现剩下的部分。普通的构造函数已经足够浅显易懂了。它们可以为引用计数和数据分配内存,并且还能设置引用计数为 1:

Handle::Handle(): u(new int(1)), p(new Point) {}
Handle::Handle(int x, int y): u(new int(1)), p(new Point(x, y)) {}
Handle::Handle(const Point& p0): u(new int(1)), p(new Point(p0)) {}

       复制构造函数和赋值操作也都很简单。当把一个句柄赋值给它自身时,通过按正确的顺序增加和减少引用计数,我们实现了所需的操作:

Handle::Handle(const Handle& h): u(h.u), p(h.p) { ++*u; }
Handle& operator=(const handle& h)
{
	++*h.u;
	if(--*u == 0){
		delete u;
		delete p;
	}
	u = h.u;
	p = h.p;
	return *this;
}

       析构函数也不难:

Handle::~Handle()
{
	if(--*u == 0){
		delete u;
		delete p;
	}
}

       所有这些都比相应的直接将引用计数绑定到 Point 上的实现要稍微复杂一些。尤其是,对 new 和 delete 使用要成双成对地出现;一个处理引用计数,另一个处理数据。根据句柄的不同,对数据的处理可能也会有所不同。有没有一种方法能抽象化我们关于引用计数的工作呢?

3.   对引用计数的抽象

       我们当然希望我们的引用计数型句柄易于实现。但是对于这样一个非常普遍的抽象来说,为了避免日后一次次反复重写,我们应该多花些功夫,不要指望能很轻松地搞定。尤其是,我们要假设引用计数类对句柄将要绑定到的那个对象的特性一无所知。这种情况下,我们能做些什么呢?

       首先,如果我们的目标是要重新实现以前所做的,那么引用计数对象就应该包含一个指向 int 的指针。而且,最起码还需要构造函数、析构函数、赋值操作符和复制构造函数:

class UseCount{
public:
	UseCount();
	UseCount(const UseCount&);
	UseCount& operator=(const UseCount&);
	~UseCount();
	// 其他需要决定的内容
private:
	int* p;
};

       实现又是怎样的呢?我们已经知道引用计数通常从 1 开始。这就告诉我们缺省构造函数应该为:

UseCount::UseCount(): p(new int(1)) {}

销毁一个 UseCount 会使计数器的值减 1,删除计数器则会返回零:

UseCount::~UseCount() { if(--*p == 0) delete p; }

现在,我们可以开始重写 Handle 类了:

class Handle{
public:
	// 和前面的一样

private:
	Point* p;
	UseCount u;
};

       由于构造函数可以依赖于缺省 UseCount 构造函数的行为,所以变得更简单了:

Handle::Handle(): p(new Point) {}
Handle::Handle(int x, int y): p(new Point(x, y)) {}
Handle::Handle(const Point& p0): p(new Point(p0)) {}
Handle::Handle(const Handle& h): u(h.u), p(h.p) {}

       复制构造函数更是简便得让人吃惊:它现在只复制 Handle 的组件,这说明完全可以省略复制构造函数来。

       析构函数是什么样子呢?它还有点问题:它需要知道引用计数是否要变成 0,以便知道要不要删除句柄的数据。让我们增加一个叫做 only 的成员,该成员描述这个特殊的 UseCount 对象是不是唯一指向它的计数器的对象:

class UseCount{
	// 和前面的一样

public:
	bool only();
};

bool UseCount::only() { return *p == 1; }

       现在,我们可以写一个新的 Handle 析构函数:

Handle::~Handle()
{
	if(u.only())
		delete p;
}

       Handle 赋值操作符的情况又如何呢?它需要几种类 UseCount 不直接支持的操作:对一个计数器的值增 1,对另一个的值减 1,可能还要删除一个计数器。另外,我们以后还要决定是否删除已赋值的数据。因为这些操作中的很多都可能随意改变引用计数,所以应该在类 UseCount 中增加另一种操作。由于这种操作实现的特殊性,不容易找到一个合适的名字,reattach 似乎也一般。进行到这里,我们将私有化对 UseCount 对象的设置。这样,我们就不必考虑对 UseCount 赋值的含义了:

class UseCount{
	// 和前面的一样

public:
	bool reattach(const UseCount&);

private:
	UseCount& operator=(const UseCount&);
};

       下面是对 reattach 的定义:

bool UseCount::reattach(const UseCount& u)
{
	++*u.p;
	if(--*p == 0){
		delete p;
		p = u.p;
		return true;
	}
	p = u.p;
	return false;
}

       现在,我们可以完成对句柄的赋值:

Handle& Handle::operator=(const Handle& h)
{
	if(u.reattach(h.u))
		delete p;
	p = h.p;
	return *this;
}

4.   存取函数和写时复制

       剩下的就是对 Point 对象的单个元素的读取和写入了。和第一部分一样,我们将通过写时复制实现值语义,这样,在改变 Point 的组件之前,要确定它的句柄是当前唯一使用该特定 Point 对象的句柄。成员 UseCount::only 帮助我们查出某个句柄是否是当前唯一使用这个句柄对象的句柄,但是它不能提供一种方法使我们能够强制这个句柄成为唯一的一个。所以,需要另一个 UseCount 成员的帮助。和 reattach 相似,它对引用计数进行适当的控制,并返回一个说明要不要复制对象本身的结果:

class UseCount{
	// 和前面的一样

public:
	bool makeonly();
};

       关于 makeonly 的定义很直观:

bool UseCount::makeonly(){
	if(*p == 1)
		return false;
	--*p;
	p = new int(1);
	return true;
}

       现在,我们可以写存取函数了。这里只说明关于 x 的存取函数,关于 y 的存取函数与此类似:

int Handle::x() const{
	return p->x();
}

Handle& Handle::x(int x0){
	if(u.makeonly())
		p = new Point(*p);
	p->x(x0);
	return *this;
}

 

总结:

       作者设计类的思想方法很值得借签。

       第一部分和第二部分所描述的句柄都是通过引用计数实现的,不同是第一部分的句柄只能用于特定类,它把对象和引用计数封装成一个新的类,句柄类通过定义这个新类的指针来对其控制;而第二部分的句柄可以实现继承层次所有类的控制,它成对地控制基类指针和引用计数指针成员,增加一个辅助类来实现引用计数部分的功能,可以大大简化这种句柄类的定义。
                   

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值