公告:本站提供编程开发方面的技术交流与分享,打造最佳教程网,希望能为您排忧解难!

Effective C++第28条:不要返回指向对象内部部件的“句柄”

Effective C++第28条:不要返回指向对象内部部件的“句柄”

更新时间:2013-05-26 12:13:10 |

第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 条)——当容器本身被销毁时,这些数据同时也会被销毁。然而,这仅仅是一个例外,不是惯例。

铭记在心

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

最大的技术交流平台 www.goodxyx.com© CopyRight 2011-2013, All Rights Reserved

浙ICP备11033019号