29.避免返回指向对象内部成分的handles

问题实例

 
假设存在一个矩形类,以左上角和右下角的一个坐标表示其形状。为了让该类尽可能小,我们决定将定义矩形的点移出该类,放在一个辅助的struct中,再用Rectangle指向它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Point{
public:
Point(int x,int y);
...
void setX(int newVal);
void setY(int newVal);
...
}
Struct RectData{
Point ulhc;//upper left-hand corner
Point lrhc;//lower right-hand corner
};
class Rectangle{
...
private:
shared_ptr<RectData> pData;
}

显然,用户需要了解矩形的范围,因此矩形类有成员函数如下:
1
2
3
4
class Rectangle{
Point& upperLeft() const {return pData->ulhc;}//返回引用
Point& lowerRight() const {return pData->lrhc;}
}

这种设计是错误的,因为存在自我矛盾。


案例分析

bitwise constness

在上述代码中,我们用const修饰函数,因为我们希望该函数不能修改point。但是事实上,尽管该函数并未修改数据,但是我们却可以通过其返回值修改对象的private数据,从而丧失了const性质。(详见Effective C++ 4 bitwise constness与logical constness)

指向对象内部成分的handles

这个案例提供了两个教训:

  1. 成员变量的封装性最多只等于返回其reference的函数的访问级别
    在上述代码中,尽管ulhc与lrhc被声明为private,但它们实际上是public,因为我们可以通过public成员函数获取其reference。
  2. 如果const函数返回了一个reference,那么该函数的调用者可以修改数据,不受const的制约。
    这个道理很简单,函数本身受到了const制约,但其返回值却可以自由修改。

handles定义

并非仅有reference是handles,指针、迭代器都属于handles,它们都具备返回一个“代表对象内部数据”的能力。

对象内部成分

不仅仅成员变量才算是某个对象的内部成分,被声明为protected与private的成员函数同样算是对象的内部成分,注意不要返回它们的handles,因为如果你这么做了,后者的访问级别可能会得到提高。(不再受到protected、private的保护,而是可能任由客户调用)


解决方案

 
为了避免封装性遭到大幅度破坏,我们针对此案例的解决方法是:使用const来修饰返回的handles。我们出让了对象内部的读取权,写入仍然是被禁止的。

空悬handle

但即使如此,返回对象内部成分的handles还有可能造成其他问题,比如dangling handles(所指向的对象不复存在)。其最常见翻车现场就是函数返回值。
假设某个函数返回一个GUI对象的外框,其外框是一个矩形:

1
2
class GUIObject{...};
const Rectangle boundingBox(const GUIObject& obj);

客户可能会这么使用它:
1
2
3
GUIobject *pgo;
...
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());

在本次使用中,boundingBox函数建立了一个局部无名的Rectangle对象,并将其内部handles绑定到了一个指针中。关键在于,当该语句执行结束后,无名对象被销毁,可pUpperLeft仍然指向着其内部成分,也就是说,pUpperLeft成为了一个空悬指针。


总结

 
函数返回一个指向对象内部成分的handle总是带有风险的,但这并不意味着你绝不可以返回handle,比如operator[]总是返回容器内部元素的reference.但这样的函数终究是例外而非常态。
如果无法避免的话,记得为const成员函数的返回值也添上const属性。