Item 28: 避免返回对象内部构件的“句柄”
假设你正在一个包含矩形的应用程序上工作。每一个矩形都可以用它的左上角和右下角表示出来。为了将一个 Rectangle 对象保持在较小状态,你可能决定那些点的定义的域不应该包含在 Rectangle 本身之中,更合适的做法是放在一个由 Rectangle 指向的辅助的结构体中:
class Point { // class for representing points
public:
Point(int x, int y);
...
void setX(int newVal);
void setY(int newVal);
...
};
struct RectData { // Point data for a Rectangle
Point ulhc; // ulhc = " upper left-hand corner"
Point lrhc; // lrhc = " lower right-hand corner"
};
class Rectangle {
...
private:
std::tr1::shared_ptr<RectData> pData; // see Item 13 for info on
}; // tr1::shared_ptr
由于 Rectangle 的客户需要有能力操控 Rectangle 的区域,因此类提供了 upperLeft 和 lowerRight 函数。可是,Point 是一个用户定义类型,所以,留心 Item 20 关于在典型情况下,以传引用的方式传递用户定义类型比传值的方式更加高效的观点,这些函数返回引向底层 Point 对象的引用:
class Rectangle {
public:
...
Point& upperLeft() const { return pData->ulhc; }
Point& lowerRight() const { return pData->lrhc; }
...
};
这个设计可以编译,但它是错误的。实际上,它是自相矛盾的。一方面,upperLeft 和 lowerRight 是被声明为 const 的成员函数,因为它们被设计成仅仅给客户提供一个获得 Rectangle 的点的方法,而不允许客户改变这个 Rectangle(参见 Item 3)。另一方面,两个函数都返回引向私有的内部数据的引用——调用者可以利用这些引用修改内部数据!例如:
Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec(coord1, coord2); // rec is a const rectangle from
// (0, 0) to (100, 100)
rec.upperLeft().setX(50); // now rec goes from
// (50, 0) to (100, 100)!
请注意这里,upperLeft 的调用者是怎样利用返回的 rec 的内部 Point 数据成员的引用来改变这个成员的。但是 rec 却被期望为 const!
这直接引出两条经验。第一,一个数据成员被封装,但是具有最高可访问级别的函数还是能够返回引向它的引用。在当前情况下,虽然 ulhc 和 lrhc 被声明为 private,它们还是被有效地公开了,因为 public 函数 upperLeft 和 lowerRight 返回了引向它们的引用。第二,如果一个 const 成员函数返回一个引用,引向一个与某个对象有关并存储在这个对象本身之外的数据,这个函数的调用者就可以改变那个数据(这正是二进制位常量性的局限性(参见 Item 3)的一个副作用)。
我们前面做的每件事都涉及到成员函数返回的引用,但是,如果它们返回指针或者迭代器,因为同样的原因也会存在同样的问题。引用,指针,和迭代器都是句柄(handle)(持有其它对象的方法),而返回一个对象内部构件的句柄总是面临危及对象封装安全的风险。就像我们看到的,它同时还能导致 const 成员函数改变了一个对象的状态。
我们通常认为一个对象的“内部构件”就是它的数据成员,但是不能被常规地公开访问的成员函数(也就是说,它是 protected 或 private 的)也是对象内部构件的一部分。同样地,不要返回它们的句柄也很重要。这就意味着你绝不应该有一个成员函数返回一个指向拥有较小的可访问级别的成员函数的指针。如果你这样做了,它的可访问级别就会与那个拥有较大的可访问级别的函数相同,因为客户能够得到指向这个拥有较小的可访问级别的函数的指针,然后就可以通过这个指针调用这个函数。
无论如何,返回指向成员函数的指针的函数是难得一见的,所以让我们把注意力返回到 Rectangle 类和它的 upperLeft 和 lowerRight 成员函数。我们在这些函数中挑出来的问题都只需简单地将 const 用于它们的返回类型就可以排除:
class Rectangle {
public:
...
const Point& upperLeft() const { return pData->ulhc; }
const Point& lowerRight() const { return pData->lrhc; }
...
};
通过这个修改的设计,客户可以读取定义一个矩形的 Points,但他们不能写它们。这就意味着将 upperLeft 和 upperRight 声明为 const 不再是一句空话,因为他们不再允许调用者改变对象的状态。至于封装的问题,我们总是故意让客户看到做成一个 Rectangle 的 Points,所以这是封装的一个故意的放松之处。更重要的,它是一个有限的放松:只有读访问是被这些函数允许的,写访问依然被禁止。
虽然如此,upperLeft 和 lowerRight 仍然返回一个对象内部构件的句柄,而这有可能造成其它方面的问题。特别是,这会导致空悬句柄:引用了不再存在的对象的构件的句柄。这种消失的对象的最普通的来源就是函数返回值。例如,考虑一个函数,返回在一个矩形窗体中的 GUI 对象的 bounding box:
class GUIObject { ... };
const Rectangle // returns a rectangle by
boundingBox(const GUIObject& obj); // value; see Item 3 for why
// return type is const
现在,考虑客户可能会这样使用这个函数:
GUIObject *pgo; // make pgo point to
... // some GUIObject
const Point *pUpperLeft = // get a ptr to the upper
&(boundingBox(*pgo).upperLeft()); // left point of its
// bounding box
对 boundingBox 的调用会返回一个新建的临时的 Rectangle 对象。这个对象没有名字,所以我们就称它为 temp。于是 upperLeft 就在 temp 上被调用,这个调用返回一个引向 temp 的一个内部构件的引用,特别是,它是由 Points 构成的。随后 pUpperLeft 指向这个 Point 对象。到此为止,一切正常,但是我们无法继续了,因为在这个语句的末尾,boundingBox 的返回值—— temp ——被销毁了,这将间接导致 temp 的 Points 的析构。接下来,剩下 pUpperLeft 指向一个已经不再存在的对象;pUpperLeft 空悬在创建它的语句的末尾!
这就是为什么任何返回一个对象的内部构件的句柄的函数都是危险的。它与那个句柄是指针,引用,还是迭代器没什么关系。它与是否受到 cosnt 的限制没什么关系。它与那个成员函数返回的句柄本身是否是 const 没什么关系。全部的问题在于一个句柄被返回了,因为一旦这样做了,你就面临着这个句柄比它引用的对象更长寿的风险。
这并不意味着你永远不应该让一个成员函数返回一个句柄。有时你必须如此。例如,operator[] 允许你从 string 和 vector 中取出单独的元素,而这些 operator[]s 就是通过返回引向容器中的数据的引用来工作的(参见 Itme 3)——当容器本身被销毁,数据也将销毁。尽管如此,这样的函数属于特例,而不是惯例。
Things to Remember
避免返回对象内部构件的句柄(引用,指针,或迭代器)。这样会提高封装性,帮助 const 成员函数产生 cosnt 效果,并将空悬句柄产生的可能性降到最低。