第28条:
不要返回指向对象内部部件的“句柄”
假设你正在设计一个与矩形相关的应用程序。每个矩形的区域都由它的左上角和右下角的坐标来表示。为了
让
Rectangle
对象尽可能的小巧,你可能会做出这样的决定:
Rectangle
自身并不保存这些点的坐标的信息,取而代之的是将这些信息保存在一个辅助结构中,然后让
Rectangle
指向它:
class Point { //
表示点的类
public:
Point(int x, int y);
...
void setX(int newVal);
void setY(int newVal);
...
};
struct RectData { //
供
Rectangle
类使用的点的数据
Point ulhc; // ulhc = "
左上角点的坐标
Point lrhc; // lrhc = "
右下角点的坐标
"
};
class Rectangle {
...
private:
std::tr1::shared_ptr<RectData> pData;
//
关于
tr1::shared_ptr
请参见第
13
条
};
因为
Rectangle
的客户端程序员可能需要了解矩形的区域,所以这个类就应该提供
upperLeft
和
lowerRight
函数。然而,
Point
却是一个用户自定义的类型,因此你可能会回忆起第
20
条的经验:通过引用传递用户自定义类型的对象要比直接传值更高效,这些函数可以返回引用来指向更底层的
Point
对象:
class Rectangle {
public:
...
Point& upperLeft() const { return pData->ulhc; }
Point& lowerRight() const { return pData->lrhc; }
...
};
这样的设计可以通过编译,但是它却是错误的。实际上,它是自我矛盾的。另外,由于
upperLeft
和
lowerRight
的设计初衷仅仅是为客户端程序员提供一个途径来了解
Rectangle
的两个顶点坐标在哪里,而不是让客户端程序员去修改它,因此这两个函数应声明为
const
成员函数。另外,这两个函数都返回指向私有内部数据的引用——通过这些引用,调用者可以任意修改内部数据!请看下边的示例:
Point coord1(0, 0);
Point coord2(100, 100);
const
Rectangle rec(coord1, coord2);// rec
是一个
Rectangle
常量
//
两顶点是
(0, 0), (100, 100)
rec.upperLeft().setX(50);
//
但现在
rec
的两顶点却变为
// (50, 0), (100, 100)!
upperLeft
返回了
rec
内部的
Point
数据成员,在这里请注意:虽然
rec
本身应该是
const
的,但是调用者竟可以使用
upperLeft
所返回的引用来修改这个数据成员!
上面的现象立刻引出了两个议题:首先,数据成员仅仅与访问限制最为宽泛的函数拥有同等的封装性。在这种情况下,即使
ulhc
和
lrhc
声明为私有的,它们实际上仍然是公共的,这是因为公共函数
upperLeft
和
lowerRight
返回了指向它们的引用。其次,如果一个
const
成员函数返回一个引用,这一引用指向的数据与一个对象相关,但这一数据却保存在该对象以外,那么函数的调用者就可以修改这一数据。(这样恰巧超出了按位恒定的范畴——参见第
3
条。)
我们所做的一切都与返回引用的成员函数有关,但是如果它们返回的是指针或者迭代器,同样的问题仍然会因为同样的理由发生。引用、指针、迭代器都可以称作“句柄”(获取其它对象的渠道),返回一个指向对象内部部件的句柄,通常都会危及到对象的封装性。就像我们看到的,即使成员函数是
const
的,返回对象的状态也是可以任意更改的。
大体上讲,对象的“内部部件”主要是它的数据成员,但是非公用的成员函数同样也是对象的内部部件。与数据成员相同,返回指向成员函数的句柄也是糟糕的设计。这意味着你不应该让一个公用成员函数
A
返回一个指向非公用成员函数
B
的指针。如果你这样做了,
B
的访问权层次就与
A
一样了,这是因为客户端程序员将能够取得
B
的指针,然后通过这一指针来调用它。
索性的是,返回指向成员函数指针的函数并不常见,所以让我们还是把精力放在
Rectangle
类和他的
upperLeft
和
lowerRight
成员函数上来吧。我们所发现的关于这些函数所存在的两个问题都可以简单的解决,只要将它们的返回值限定为
const
的就可以了:
class Rectangle {
public:
...
const Point& upperLeft() const { return pData->ulhc; }
const Point& lowerRight() const { return pData->lrhc; }
...
};
使用这一改进的设计方案,客户端程序员就可以读取用来定义一个矩形的两个点,但是他们不可以修改这两个点。这就意味着将
upperLeft
和
lowerRight
声明为
const
的并不是一个假象,因为它们将不允许调用者来修改对象的状态。至于封装问题,我们一直坚持让客户端程序员能能够看到构造一个
Rectangle
的两个
Point
,所以说这里我们故意放松了封装的限制。更重要的是,这一放松是有限的:这些函数仅仅提供了读的访问权限。写权限仍然是禁止的。
class GUIObject { ... };
const Rectangle
boundingBox(const GUIObject& obj);
//
以传值方式返回一个矩形。关于返回值为什么是
const
的,请参见第
3
条
现在请考虑一下客户端程序员可能怎样来使用这个函数:
GUIObject *pgo; //
让
pgo
指向某个
GUIObject
const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft());
//
取得一个指向
boundingBox
左上角点的指针
调用
boundingBox
将会返回一个新的、临时的
Retangle
对象。这个对象没有名字,所以姑且叫它
temp
。随后
temp
将调用
upperLeft
,然后此次调用将返回一个指向
temp
内部部件的引用,特别地,指向构造
temp
的一个点。
pUpperleft
将会指向这一
Point
对象。到目前为止一切都很完美,但是任务尚未完成,因为在这一语句的最后,
boundingBox
的返回值——
temp
——将会被销毁,这样间接上会导致
temp
的
Point
被销毁掉。于是,
pUpperLeft
将会指向一个并不存在的对象。这条语句创建了
pUpperLeft
,可也让它成了孤魂野鬼。
为什么说:任何返回指向对象内部部件句柄的函数都是危险的,这个问题已经一目了然了。至于句柄是指针还是引用还是迭代器,函数是否是
const
的,成员函数返回的句柄本身是不是
const
的,这一切都无关紧要。只有一点,那就是:只要返回了一个句柄,那么就意味着你正在承担风险:它可能会比它指向的对象存活更长的时间。
这并不意味着你永远也不能让一个成员函数返回一个句柄。有些时候你不得不这样做。比如说,
operator[]
允许你获取
string
和
vector
中的任一元素,这些
operator[]
的工作就是通过返回容器内部的数据来完成的(参见第
3
条)——当容器本身被销毁时,这些数据同时也会被销毁。然而,这仅仅是一个例外,不是惯例。
铭记在心
l
避免返回指向对象内部部件的句柄(引用、指针或迭代器)。这样做可以增强封装性,帮助
const
成员函数拥有更加“
const
”的行为,并且使“野句柄”出现的几率降至最低。