14

Scott Meyers 编写的 Effective C++在第 5 章第 28 条中指出要避免将“句柄”(指针、引用或迭代器)返回到对象内部,这绝对是一个好点。

即不要这样做:

class Family
{
public:
    Mother& GetMother() const;
}

因为它破坏了封装并允许更改私有对象成员。

甚至不要这样做:

class Family
{
public:
    const Mother& GetMother() const;
}

因为它可能导致“悬空句柄”,这意味着您保留对已销毁对象成员的引用。

现在,我的问题是,有什么好的选择吗?想象妈妈很重!如果我现在返回 Mother 的副本而不是参考,GetMother 将成为一项相当昂贵的操作。

您如何处理此类案件?

4

6 回答 6

15

首先,让我重申一下:最大的问题不是生命周期,而是封装问题。

封装不仅意味着没有人可以在您不知道的情况下修改内部,封装意味着没有人知道您的类中的事物是如何实现的,因此只要您保持接口相同,您就可以随意更改类内部。

现在,你返回的引用是否const无关紧要:你不小心暴露了Mother你的类中有一个对象的事实,Family现在你无法摆脱它(即使你有更好的表示),因为你所有的客户可能会依赖它,并且必须更改他们的代码......

最简单的解决方案是按值返回:

class Family {
public:

    Mother mother() { return _mother; }
    void mother(Mother m) { _mother = m; }

private:
    Mother _mother;
};

因为在下一次迭代中,我可以在_mother不破坏界面的情况下删除:

class Family {
public:

     Mother mother() { return Mother(_motherName, _motherBirthDate); }

     void mother(Mother m) {
         _motherName = m.name();
         _motherBirthDate = m.birthDate();
     }

private:
     Name _motherName;
     BirthDate _motherBirthDate;
};

看看我是如何在不改变界面的情况下完全改造内部结构的?十分简单。

注意:显然这种转换只是为了效果......

显然,这种封装是以牺牲一些性能为代价的,这里有一个张力,每次编写 getter 时,是否应该首选封装还是性能是你的判断。

于 2012-11-01T12:33:29.017 回答
7

可能的解决方案取决于您的类的实际设计以及您认为什么是“对象内部”。

  1. Mother只是实现细节,Family可能对Family用户完全隐藏
  2. Family被认为是其他公共对象的组合

Family在第一种情况下,您应完全封装子对象并仅通过函数成员(可能复制Mother公共接口)提供对它的访问:

class Family
{
  std::string GetMotherName() const { return mommy.GetName(); }
  unsigned GetMotherAge() const { return mommy.GetAge(); }
  ...
private:
   Mother mommy;
   ...
};

好吧,如果Mother接口很大可能会很无聊,但这可能是设计问题(好的接口应该有 3-5-7 个成员),这会让你以更好的方式重新审视和重新设计它。

在第二种情况下,您仍然需要返回整个对象。有两个问题:

  1. 封装分解(最终用户代码将取决于Mother定义)
  2. 所有权问题(悬空指针/引用)

解决问题 1 使用接口而不是特定类,解决问题 2 使用共享或弱所有权:

class IMother
{
   virtual std::string GetName() const = 0;
   ...
};

class Mother: public IMother
{
   // Implementation of IMother and other stuff
   ...
};

class Family
{
   std::shared_ptr<IMother> GetMother() const { return mommy; }
   std::weak_ptr<IMother> GetMotherWeakPtr() const { return mommy; }

   ...
private:
   std::shared_ptr<Mother> mommy;
   ...
};
于 2012-11-01T12:30:40.550 回答
4

如果只读视图是您所追求的,并且由于某种原因您需要避免悬空的句柄,那么您可以考虑返回一个shared_ptr<const Mother>.

这样,Mother对象可以比对象寿命长Family。当然,它也必须存储它shared_ptr

部分考虑是您是否要使用过多shared_ptr的 s 创建引用循环。如果是,那么您可以考虑weak_ptr,也可以考虑只接受悬空句柄的可能性,但编写客户端代码来避免它。例如,没有人会过分担心std::vector::at当向量被销毁时返回的引用会变得陈旧。但是,容器是一个类的极端例子,它故意公开它“拥有”的对象。

于 2012-11-01T12:16:48.527 回答
3

这又回到了一个基本的 OO 原则:

Tell objects what to do rather than doing it for them.

你需要Mother做一些有用的事情吗?让Family对象为你做这件事。通过对象上的方法的参数将任何外部依赖项封装在一个漂亮的接口(Class在 c++ 中)中。Family

于 2012-11-01T12:16:40.937 回答
2

因为它可能导致“悬空句柄”,这意味着您保留对已销毁对象成员的引用。

您的用户也可以取消引用null或同样愚蠢的事情,但他们不会这样做,也不会这样做,只要生命周期清晰且定义明确。这没有什么问题。

于 2012-11-01T12:24:53.593 回答
0

这只是语义问题。在您的情况下,Mother不是内部结构,而不是其实现细节 类实例可以在 a 中引用,也可以在许多其他实体中引用。此外,实例生命周期甚至可能与生命周期无关。FamilyMotherFamilyMotherFamily

所以更好的设计是存储在Family一个中,并在界面shared_ptr<Mother>中公开它而不用担心。Family

于 2012-11-01T12:31:04.333 回答