问题实例
假设存在一个矩形类,以左上角和右下角的一个坐标表示其形状。为了让该类尽可能小,我们决定将定义矩形的点移出该类,放在一个辅助的struct中,再用Rectangle指向它:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class 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
4class 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
这个案例提供了两个教训:
- 成员变量的封装性最多只等于返回其reference的函数的访问级别
在上述代码中,尽管ulhc与lrhc被声明为private,但它们实际上是public,因为我们可以通过public成员函数获取其reference。 - 如果const函数返回了一个reference,那么该函数的调用者可以修改数据,不受const的制约。
这个道理很简单,函数本身受到了const制约,但其返回值却可以自由修改。
handles定义
并非仅有reference是handles,指针、迭代器都属于handles,它们都具备返回一个“代表对象内部数据”的能力。
对象内部成分
不仅仅成员变量才算是某个对象的内部成分,被声明为protected与private的成员函数同样算是对象的内部成分,注意不要返回它们的handles,因为如果你这么做了,后者的访问级别可能会得到提高。(不再受到protected、private的保护,而是可能任由客户调用)
解决方案
为了避免封装性遭到大幅度破坏,我们针对此案例的解决方法是:使用const来修饰返回的handles。我们出让了对象内部的读取权,写入仍然是被禁止的。
空悬handle
但即使如此,返回对象内部成分的handles还有可能造成其他问题,比如dangling handles(所指向的对象不复存在)。其最常见翻车现场就是函数返回值。
假设某个函数返回一个GUI对象的外框,其外框是一个矩形:1
2class GUIObject{...};
const Rectangle boundingBox(const GUIObject& obj);
客户可能会这么使用它:1
2
3GUIobject *pgo;
...
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());
在本次使用中,boundingBox函数建立了一个局部无名的Rectangle对象,并将其内部handles绑定到了一个指针中。关键在于,当该语句执行结束后,无名对象被销毁,可pUpperLeft仍然指向着其内部成分,也就是说,pUpperLeft成为了一个空悬指针。
总结
函数返回一个指向对象内部成分的handle总是带有风险的,但这并不意味着你绝不可以返回handle,比如operator[]总是返回容器内部元素的reference.但这样的函数终究是例外而非常态。
如果无法避免的话,记得为const成员函数的返回值也添上const属性。