Effective C++ 学习笔记 条款28 避免返回handles指向对象内部成分
假设你在开发一个涉及矩形的应用程序。每个矩形可以由其左上角和右下角来表示。为了让 Rectangle 对象足够小你决定不把界定矩形范围的点直接存储在 Rectangle 内部而是放在一个辅助结构体中由 Rectangle 通过指针指向它由于 Rectangle 的客户代码需要能够获知矩形的范围该类提供了 upperLeft 和 lowerRight 函数。不过Point 属于用户自定义类型而条款20指出按引用传递这类类型通常比按值传递更高效因此这两个函数返回的是底层 Point 对象的引用这个设计可以编译却有问题。事实上它还自相矛盾。一方面upperLeft 和 lowerRight 被声明为 const 成员函数——它们旨在仅向客户提供获取矩形顶点的手段而不允许客户修改 Rectangle 对象参见条款3。另一方面这两个函数都返回了引用指向内部私有数据——调用者可以利用这些引用来修改内部数据请看请注意upperLeft 的调用者能利用返回的引用去修改 rec 内部某个 Point 数据成员。可 rec 明明是 const 的啊这立刻引出了两个教训。第一数据成员的封装性取决于对外返回其引用的函数中那个“访问权限最宽松”的函数。本例中虽然 ulhc 和 lrhc 声明为 private实际上却等同于 public因为公开的 upperLeft 和 lowerRight 返回了它们的引用。第二如果 const 成员函数返回一个引用而该引用指向的对象关联着该对象本身、却存储在对象外部那么该函数的调用者就可以修改那个数据这不过是“按位常量性”局限性的一个体现——参见条款3。以上我们讨论的都是成员函数返回引用但如果返回指针或迭代器同样会存在这些问题原因也相同。引用、指针、迭代器都算作“句柄”一种获取其他对象的手段而返回对象内部成员的句柄始终会损害对象封装性。如我们所见它甚至可能让 const 成员函数也能修改对象的状态。我们通常认为对象的“内部”指它的数据成员但不可公开访问的成员函数即 protected 或 private 函数同样属于对象内部的一部分。因此返回指向它们的句柄同样不妥。这意味着你绝不该让一个成员函数返回指向另一个访问权限更低之成员函数的指针。如果这样做有效访问级别就变成了那个更公开的函数的级别——因为客户可以拿到指向低级函数的指针然后通过该指针去调用它。不过返回成员函数指针的函数并不常见所以我们还是把注意力转回 Rectangle 类及其 upperLeft 和 lowerRight 成员函数。之前提到的两个问题只需对返回类型加上 const 就能消除采用上述修改后客户代码可以读取构成矩形的那些 Point但无法写入它们。这样一来把 upperLeft 和 lowerRight 声明为 const 就不再是自欺欺人了——因为它们不再允许调用者修改对象的状态。至于封装性方面我们原本就打算让客户看到构成矩形的顶点所以这属于有意放宽封装。更重要的是这是一种有限度的放宽这些函数只授予了读权限写权限依然被禁止。即便如此upperLeft 和 lowerRight 返回的仍是对象内部成员的句柄这在其他方面仍可能引发问题。尤其是可能导致悬空句柄dangling handles——即指向的对象已经不复存在的句柄。这类“消失”的对象最常见于函数返回值。举个例子考虑这样一个函数它以矩形形式返回某个 GUI 对象的包围盒bounding box现在看看客户代码会如何使用该函数调用 boundingBox 会返回一个新的、临时的 Rectangle 对象。该对象没有名字我们不妨称之为 temp。随后 upperLeft 会在 temp 上被调用该调用返回一个引用指向 temp 内部某一部分——具体来说就是构成它的某个 Point。于是 pUpperLeft 就指向了这个 Point 对象。一切尚好但事情并未结束——因为这个语句结束时boundingBox 的返回值 temp 会被销毁这又间接导致 temp 内部的那些 Point 也被销毁。如此一来pUpperLeft 便指向了一个不复存在的对象从它被创建出来的那一句结束起pUpperLeft 就已经悬空了这就是为什么任何返回对象内部成员句柄的函数都是危险的。无论这个句柄是指针、引用还是迭代器无论是否带有 const 限定也无论返回句柄的成员函数本身是否 const——统统无关紧要。关键在于句柄被返回了因为只要这么做你就得承担句柄存活时间超过所指对象生命周期的风险。这倒不是说你永远不该让成员函数返回句柄。有时你不得不如此。例如operator[] 让你能从字符串和 vector 中取出单个元素而它们的实现方式正是返回容器内部数据的引用参见条款3——这些数据会随着容器自身销毁而销毁。但即便如此这类函数也只是特例并非通例。切记1.避免返回对象内部成员的句柄引用、指针或迭代器。这样做既能提升封装性也能让 const 成员函数真正保持常量性同时尽量减少悬空句柄的产生。