“避免将句柄返回到对象内部”,那么替代方案是什么?

时间:2012-11-01 12:08:46

标签: c++ effective-c++

Effective C++ by Scott Meyers在第5章第28项中告诉我们避免将“句柄”(指针,引用或迭代器)返回给对象内部,这绝对是一个好点。

即。不要这样做:

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

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

甚至不要这样做:

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

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

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

你如何处理这类案件?

6 个答案:

答案 0 :(得分:14)

首先,让我重新进行迭代:最大的问题不是生命周期,而是封装之一。

封装不仅意味着没有人能够在没有意识到内容的情况下修改内部,封装意味着没有人知道你的类中是如何实现的,所以你可以随意改变类内部,只要你保持界面完全相同。

现在,您返回的引用是否为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;
};

看看我是如何在不改变iota接口的情况下完全改造内部构件的?容易Peasy。

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

显然,这种封装是以某种性能为代价的,这里有一种紧张,每次你写一个吸气剂时,你的判断是否应该首选封装或性能。

答案 1 :(得分:5)

可能的解决方案取决于您的课程的实际设计以及您认为“对象内部”的内容。

  1. Mother只是Family的实施细节,可以完全隐藏Family用户
  2. Family被视为其他公共对象的组合
  3. 在第一种情况下,您应完全封装子对象并仅通过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. 所有权问题(悬挂指针/参考)
    3. 要解决问题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;
         ...
      };
      

答案 2 :(得分:4)

如果您正在阅读只读视图,并且由于某种原因您需要避免悬空手柄,那么您可以考虑返回shared_ptr<const Mother>

这样,Mother对象可以超出Family对象。当然,这也必须由shared_ptr存储。

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

答案 3 :(得分:3)

这可以追溯到一个基本的OO原则:

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

您需要Mother做一些有用的事情吗?请Family对象为您执行此操作。通过Class对象上方法的参数,将它包含在一个漂亮的接口(c ++中的Family)中。

答案 4 :(得分:2)

  

因为它可以导致“悬挂手柄”,这意味着你保持一个   引用已经销毁的对象的成员。

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

答案 5 :(得分:0)

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

因此,更好的设计将存储在Family shared_ptr<Mother>中,并在Family界面中公开,无需担心。