《Effective C++ 中文版 第三版》读书笔记
** 条款 28:避免返回 handle 指向对象内部成分 **
假设程序涉及矩形。为了让 Rectangle 对象尽可能小,可能把定义矩形的点放在一个辅助的 struct 内再让 Rectangle 去指它:
class Point{
public:
Point(int x, int y);
...
void setX(int newVal);
void setY(int newVal);
...
};
struct RectData{
Point ulhc;
Point lrhc;
};
class Rectangle{
...
Point& upperLeft()const {return pData->ulhc;}
Point& lowerRight()const {return pData->lrhc;}
private:
std::tr1::shared_ptr<RectData> pData;
};
这样的设计可以通过编译,但却是错误的。实际上自相矛盾,一方面 upperleft 和 lowerRight 被声明为 const,不让客户修改 Rectangle。另一方面,这两个函数都返回 reference 指向 private 数据,调用者可以通过这些 reference 更改内部数据:
Point coord1(0,0);
Point coord2(100,100);
const Rectangle rec(coord1, coord2);
rec.upperLeft().setX(50); // 现在 rec 变成从 (50,0) 到 (100,100)
第一,成员变量的封装性最多只等于 “返回其 reference” 的函数的访问级别。
第二,如果 const 成员函数传出一个 reference,后者所指数据与对象自身有关,而它又被存储在对象之外,那么这个函数的调用者可以修改那笔数据。这正是 bitwise constness 的一个附带结果,条款 3。
如果它们返回的是指针或迭代器,相同的结果还会发生,原因相同。reference、指针和迭代器统统都是所谓的 handles(号码牌,用来取得某个对象),而返回一个“代表对象内部数据的 handle”,随之而来的便是 “降低对象封装性” 的风险。同时,也可能造成 “虽然调用 const 成员函数却造成对象状态被更改”。
通常我们认为,对象的“内部”就是指它的成员变量,其实不被公开使用的成员函数(protected 或 private)也是对象 “内部” 的一部分,所以也不该返回它们的 handles。否则,它们的访问级别就会提高到返回它们的成员函数的访问级别。
上述两个问题可以在它们的返回类型上加上 const 即可:
class Rectangle{
...
const Point& upperLeft()const {return pData->ulhc;}
const Point& lowerRight()const {return pData->lrhc;}
private:
std::tr1::shared_ptr<RectData> pData;
};
const 不在是个谎言。至于封装性,这里是蓄意放松封装,有限度的放松:只让渡读取权,涂写权是禁止的。
即使这样,返回 “代表对象内部” 的 handles,有可能在其他场合导致 dangling handles(空悬的号码牌):
这种 handle 所指东西(的所属对象)不复存在。这种 “不复存在的对象” 最常见的来源就是函数返回值。
class GUIObject{...};
const Rectangle boundingBox(const GUIObject& obj);
现在,客户可能这么使用这个函数:
GUIObject *pgo;
...
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());//取得一个指针指向外框左上点
对 boundingBox 的调用获得一个新的、暂时的 Rectangle 对象,这个对象没有名称,权且称它为 temp。随后 upperLeft 作用于 temp 对象身上,返回 reference 指向 temp 的一个内部成分。具体指向 temp 的那个 Point 对象。但是这个语句结束之后,boundingBox 的返回值,也就是我们所说的 temp,将被销毁,而那间接导致 temp 内的 Points 析构。最终导致 pUpperLeft 指向一个不再存在的对象;变成空悬、虚吊(dangling)!
只要 handle 被传出去了,不管这个 handle 是不是 const,也不论返回 handle 的函数是不是 const。这里的唯一关键是暴露在 “handle 比其所指对象更长寿” 的风险下。
** 请记住: **
避免返回 handle(包括 reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助 const 成员函数的行为像个 const,并将发生“虚吊号码牌”的可能性降至最低。