20180316 C++ 避免返回handles指向对象内部成分
假设你的程序涉及矩阵。每个矩阵由其左上角和右下角表示,为了让一个矩阵(Rectangle)对象尽可能小,你可能会决定不把定义矩阵的这些点存放在矩阵(Rectangle)对象内,而是放在一个辅助是结构体(struct)内再让矩阵(Rectangle)去指向它:
class Point{//这个class用来表示“点”
public:
Point(int x,int y);
...
void setX(int newVal);
void setY(int newVal);
...
};
struct RectData{//这些点数据用来表现一个矩阵
Point ulhc; //ulhc = "upper left-hand corner" 即左上角
Point lrhc; //lrhc = "lower right-hand corner" 即右下角
};
class Rectangle{
...
private:
std::tr1::shared_ptr<RectData> pData;
}
矩阵的客户必须能够计算矩阵的范围,所以这个类提供upperLeft函数和lowRight函数,Point是个用户自定义类型,因为以by reference方式传递用户自定义类型往往比以by value方式传递更高效,所以这些函数返回references,代表底层的Point对象:
class Rectangle{
public:
...
Point upperLeft() const {return pData->ulhc;}
Point lowRight() const {return pData->lrhc;}
}
这样的设计可通过编译,但确实错误的,实际上它是自我矛盾的,一方面upperLeft和lowRight被声明为const成员函数,因为他们的目的只是为了提供客户一个得知Rectangle相关坐标点的方法,而不是让客户修改Rectangle。另一方面两个函数却都返回reference指向private内部数据,调用者于是可通过这些reference更改内部数据。例如:
Point coord1(0,0);
Point coord2(100,100);
const Rectangle rec(coord1,coord2);
/*rec是个const矩阵,从(0,0)到(100,100)*/
rec.upperLeft().sexX(50);
/*现在rec却变成从(50,0)到(100,100)*/
这里请注意,upperLeft的调用者能够使用被返回的reference(指向rec内部的Point成员变量)来更改成员。但rec其实应该是不可变的(const)。
上面所发生的事是由于“成员函数返回reference”。若他们返回的是指针或迭代器,相同的情况还是发生,原因也相同。
当我们把注意力集中于Rectangle class和他的upperLeft以及lowerRight成员函数。我们在这些函数身上遭遇的两个问题就可以轻松去除,只要对它们的返回类型加上const即可:
class Rectangle{
public:
...
const Point& upperLeft() const {return pData->ulhc;}
const Point& lowerRight() const {return pData->lrhc;}
...
}
有了这样的改变,客户可以读取矩阵的Point,但不能涂改它们。但即使如此,upperLeft和lowerRight还是返回了“代表对象内部”的handles,有可能在其他场合带来问题,更明确的说,他可能导致空悬的号码牌(dangling handles):这种handles所指的东西(的所属对象)不复存在。这种“不复存在的对象”最常见的来源就是函数返回值。
这并不意味着你绝对不可以让成员函数返回handles。有时候你必须那样做。例如operator[]就允许你"采摘"strings和vectors的个别元素,而这些operator[]s就是返回references指向“容器内的数据”,那些数据会随着容器被销毁而销毁。尽管如此,这样的函数毕竟是例外,不是常态。
注意:避免返回handles(包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性将至最低。