【转】http://www.cppblog.com/tiandejian/archive/2007/09/23/ec_28.html
第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 ”的行为,并且使“野句柄”出现的几率降至最低。