洛译小筑

别来无恙,我的老友…
随笔 - 45, 文章 - 0, 评论 - 172, 引用 - 0
数据加载中……

[ECPP读书笔记 条目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的客户可能需要计算矩形的面积,所以这个类就应该提供upperLeftlowerRight函数。然而,Point却是一个用户自定义的类型,因此伴着你对条目20中相关推论的回忆——通过引用传递用户自定义类型的对象要比直接传值更高效,你会让这些函数返回指向Point的引用:

class Rectangle {

public:

  ...

  Point& upperLeft() const { return pData->ulhc; }

  Point& lowerRight() const { return pData->lrhc; }

  ...

};

这样的设计可以通过编译,但是它却是错误的。实际上,它是自我矛盾的。一方面,由于upperLeftlowerRight的设计初衷仅仅是为客户提供一个途径来了解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所返回的引用来修改这个数据成员!

上面的现象立刻引出了两个议题:首先,数据成员仅仅与访问限制最为宽泛的、返回该数据成员引用的函数拥有同等的封装性。在这种情况下,即使ulhclrhc声明为私有的,它们实际上仍然是公共的,这是因为公共函数upperLeftlowerRight返回了指向它们的引用。其次,假设一个const成员函数返回一个引用,这一引用指向的数据与某个对象相关,但该数据却保存在该对象以外,那么函数的调用者就可以修改这一数据。(这仅仅是按位恒定限制所留下的隐患之一——参见条目3。)

我们所做的一切都是围绕着返回引用的成员函数展开的,但是如果成员函数返回的是指针或者迭代器,同样的问题仍然会因为同样的理由发生。引用、指针、迭代器都可以称作“句柄”(获取其它对象的渠道),返回一个指向对象内部部件的句柄,通常都会危及到对象的封装性。就像我们看到的,即使成员函数是const的,返回对象的状态也是可以任意更改的。

大体上讲,对象的“内部部件”主要是它的数据成员,但是非公有成员函数(也就是protectedprivate的)同样也是对象的内部部件。与数据成员相同,返回指向成员函数的句柄也是糟糕的设计。这意味着你不应该让一个公有成员函数A返回一个指向非公用成员函数B的指针。如果你这样做了,B的访问权层次就与A一样了,这是因为客户将能够取得B的指针,然后通过这一指针来调用它。

所幸的是,返回指向成员函数指针的函数并不常见,所以我们还是把精力放在Rectangle类和它的upperLeftlowerRight成员函数上。我们所发现的关于这些函数所存在的两个问题都可以简单的解决,只要将它们的返回值限定为const的就可以了:

class Rectangle {

public:

  ...

  const Point& upperLeft() const { return pData->ulhc; }

  const Point& lowerRight() const { return pData->lrhc; }

  ...

};

使用这一改进的设计方案,客户就可以读取用来定义一个矩形的两个顶点,但是他们不可以修改这两个Point。这就意味着将upperLeftlowerRight声明为const并不是一个假象,因为它们将不允许调用者来修改对象的状态。至于封装问题,由于我们一直期望客户能够看到构造一个Rectangle的两个Point,所以这里我们故意放松了封装的限制。更重要的是,这一放松是有限的:这些函数仅仅提供了读的访问权限。写权限仍然是禁止的。

即使这样,upperLeftlowerRight仍然会返回指向对象内部部件的句柄,而且会存在其他形式的问题。在某些特定的情况下,会导致悬空句柄:即引用对象中不再存在的某些部分的句柄。能够造成此类“会消失的对象”最普遍的东西就是函数返回值。举例说,请考虑以下函数,它以一个长方形的形式返回一个GUI对象的边界盒:

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)将会被销毁,这样间接上会导致tempPoint被销毁掉。于是,pUpperLeft将会指向一个并不存在的对象。这条语句创建了pUpperLeft,可也让它成了悬空指针。

这就解释了为什么说:任何返回指向对象内部部件句柄的函数都是危险的。至于句柄是指针还是引用还是迭代器,函数是否是const的,成员函数返回的句柄本身是不是const的,这一切都无关紧要。只有一点,那就是:只要返回了一个句柄,那么就意味着你正在承担风险:它可能会比它指向的对象存活更长的时间。

这并不意味着你永远也不能让一个成员函数返回一个句柄。有些时候你不得不这样做。比如说,operator[]允许你获取stringvector中的任一元素,这些operator[]的工作就是通过返回容器内部的数据来完成的(参见条目3)——当容器本身被销毁时,这些数据同时也会被销毁。然而,这仅仅是一个例外,不是惯例。

时刻牢记

避免返回指向对象内部部件的句柄(引用、指针或迭代器)。这样做可以增强封装性,帮助const成员函数拥有更加恒定的行为,并且使悬空句柄出现的几率降至最低。

posted on 2007-09-23 23:25 ★ROY★ 阅读(1085) 评论(2)  编辑 收藏 引用 所属分类: Effective C++

评论

# re: 【读书笔记】[Effective C++第3版][第28条]尽量不要使用类型转换  回复  更多评论   

2007-09-24 09:17 | dfl

# re: 【读书笔记】[Effective C++第3版][第28条]尽量不要使用类型转换  回复  更多评论   

很有道理
2007-09-24 09:19 | 金庆

只有注册用户登录后才能发表评论。
【推荐】超50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理