一. 内容
-
举个例子,假如你的程序涉及矩形,每个矩形由左上角和右下角坐标表示,然后采用 pimpl 手法,以及之前学过的条款知识。
namespace RectangleStuff { class Point { public: Point(int mX, int mY): X(mX), Y(mY) {} public: void SetX(int mX) { X = mX; } int GetX() { return X; } void SetY(int mY) { Y = mY; } int GetY() { return Y; } private: int X; int Y; }; struct RectangleData { Point LeftUp; Point RightDown; }; class Rectangle { public: Rectangle(const Point& mLeftUp, const Point& mRightDown): Data(new RectangleData({mLeftUp, mRightDown})) { } public: Point& GetLeftUp() const { return Data->LeftUp; } Point& GetRightDown() const { return Data->RightDown; } private: std::shared_ptr<RectangleData> Data; }; inline void Try() { const Point CoordOne(0, 100); const Point CoordTwo(100, 0); const Rectangle Rectangle(CoordOne, CoordTwo); std::cout << Rectangle.GetLeftUp().GetX() << "\n"; std::cout << Rectangle.GetRightDown().GetX() << "\n"; Rectangle.GetLeftUp().SetX(1); std::cout << Rectangle.GetLeftUp().GetX() << "\n"; } }
-
注意 明明在 Try 函数中我们定义的
const 变量,但在之后的语句中我们可以轻易修改 Rectangle 中的成员变量
,观察 Rectangle 的 Getter 函数实现,发现我们返回的是内部数据的引用,那些 Point 变量的引用,调用函数者便可借机修改它们。 -
这立刻带给我们两个教训:
- 成员变量的封装性
最多等于返回其引用的函数的访问级别
。本例中的 Data 声明为 private,但它们实际上却是 public。 - 如果 const 成员函数
返回一个引用,那么实际上调用者是可以修改其中的数据的
。这是 bitwise constness 的附带结果。
- 成员变量的封装性
-
上述所说的每一件事情,对于指针或迭代器同样适用,原因也相同。
引用,指针,迭代器统统都是所谓的 handles,用来取到某个对象
。而返回一个代表内部数据的 handle,随之而来的便是降低对象封装性的风险。同时,一如所见,它也可以导致虽然调用 const 成员函数却造成对象被修改的结果。 -
也许你可以说加上常量性的 handles。但尽管如此,仍然会带来一些问题。更明确的说,
它们会导致空悬的 handles
:这种 handles 所指东西不复存在。比如对于 Rectangle 的 GetLeftUp 函数调用会返回一个 内部数据LeftUp 的 const-reference,我们将返回值保存在一个缓存对象中,如果此时 Rectangle 是一个匿名对象,该语句执行完会自动析构,那么缓存对象所保存的 const-reference 实际上已经不存在任何事物了。这就是为什么函数如果返回一个 handle 代表对象内部成分总是危险的原因。 -
当然这并不意味着绝对不可以让成员函数返回 handle。有时候必须那样做,例如
operator[ ]就允许你使用 strings 或 vectors 内的成员
,返回它们的引用。但这些函数是例外,并不是常态。
二. 总结
- 避免返回 handles,包括 references,指针,迭代器指向对象内部,遵守这个条约可增加封装性,帮助 const 成员函数的行为像个 const,并将发生虚吊号码牌(dangling handles)的可能性降到最低。