封装集合是一种好的(正确的)方法吗?

时间:2008-09-20 14:56:36

标签: c++ collections encapsulation

class MyContainedClass {
};

class MyClass {
public:
  MyContainedClass * getElement() {
    // ...
    std::list<MyContainedClass>::iterator it = ... // retrieve somehow
    return &(*it);
  }
  // other methods
private:
  std::list<MyContainedClass> m_contained;
};

虽然msdn说std::list不应该在删除或插入时执行元素的重定位,但它是一种返回指向列表元素的指针的好方法吗?

PS:我知道我可以使用指针集合(并且必须在析构函数中使用delete元素),共享指针集合(我不喜欢)等等。

9 个答案:

答案 0 :(得分:1)

我没有看到封装这个的用法,但这可能只是我。在任何情况下,返回引用而不是指针对我来说更有意义。

答案 1 :(得分:1)

一般来说,如果你的“包含类”真正包含在你的“MyClass”中,那么MyClass不应该允许外人触及其私人内容。

因此,MyClass应该提供操作包含的类对象的方法,而不是返回指向它们的指针。因此,例如,一个方法,如“增加第十个包含的对象的值”,而不是“这里是指向第十九个包含的对象的指针,可以随意使用它”。

答案 2 :(得分:1)

这取决于......

这取决于您希望课程的封装程度,以及您想隐藏或展示的内容。

我看到的代码对我来说似乎没问题。你是对的,在另一个数据/迭代器的修改/删除的情况下,std :: list的数据和迭代器不会失效。

现在,返回指针会隐藏您使用std :: list作为内部容器的事实,并且不会让用户导航其列表。返回迭代器可以更自由地为该类的用户导航此列表,但他们会“知道”他们正在访问STL容器。

这是你的选择,我想。

请注意,如果它== std :: list&lt;&gt; .end(),那么您将遇到此代码的问题,但我想您已经知道了,并且这不是此讨论的主题

尽管如此,我还是总结了以下内容:

使用const将有助于......

你返回一个非常量指针的事实让你的对象可以默默地修改他/她可以亲自动手的任何MyContainedClass,而无需告诉你的对象。

相反或返回一个指针,你可以返回一个const指针(并用const后缀你的方法)来阻止用户修改列表中的数据而不使用你认可的访问者(setElement ?)。

  const MyContainedClass * getElement() const {
    // ...
    std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow
    return &(*it);
  }

这会增加封装。

参考怎么样?

如果你的方法不能失败(即它总是返回一个有效的指针),那么你应该考虑返回引用而不是指针。类似的东西:

  const MyContainedClass & getElement() const {
    // ...
    std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow
    return *it;
  }

这与封装无关,但是...... :-P

使用迭代器?

为什么不返回迭代器而不是指针?如果对你来说,上下导航列表是可以的,那么迭代器将比指针更好,并且主要以相同的方式使用。

如果要避免用户修改数据,请将迭代器设为const_iterator。

  std::list<MyContainedClass>::const_iterator getElement() const {
    // ...
    std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow
    return it;
  }

好的一面是用户可以浏览列表。不好的一面是用户会知道它是一个std :: list,所以......

答案 3 :(得分:1)

斯科特迈耶斯在他的书Effective STL: 50 Specific Ways to Improve Your Use of the Standard Template Library中说,不值得尝试封装你的容器,因为它们都不能完全替换另一个容器。

答案 4 :(得分:1)

认真思考 ,了解你真正想要的MyClass。我注意到一些程序员只是为了习惯而为他们的集合编写包装器,无论他们是否有超出标准STL集合所满足的特定需求。如果那是你的情况,那么typedef std::list<MyContainedClass> MyClass并完成它。

如果你有想要在MyClass中实现的操作,那么封装的成功将更多地取决于你为它们提供的接口,而不是你如何提供对它的访问。基础清单。

没有冒犯意味着,但是......由于您提供的信息有限,它闻起来就像您正在撑船:暴露内部数据,因为您无法弄清楚如何实施这些操作您的客户端代码需要MyClass ...或者可能,因为您甚至知道您的客户端代码还需要哪些操作。这是尝试在需要它的高级代码之前编写低级代码的典型问题;你知道你将要使用哪些数据,但还没有确切地确定你将要用它做什么,所以你编写了一个将原始数据一直暴露到顶层的类结构。你最好在这里重新考虑你的策略。


@cos

  

当然我正在封装   MyContainedClass不仅仅是为了这个   封装我们需要更多   具体例子:

在你知道它们将被用于什么之前,你的例子几乎没有减轻我对你正在编写容器的恐惧。您的示例容器包装器 - Document - 总共有三种方法:NewParagraph()DeleteParagraph()GetParagraph(),所有这些方法都对包含的集合进行操作({{1} }),以及所有密切镜像std::list提供的“开箱即用”操作。 std::list封装了std :: list,因为客户端不需要知道它在实现中的用途......但实际上,它只不过是一个外观 - 因为你提供客户端指向存储对象的原始指针在列表中,客户端仍然隐含在实现中。

  

如果我们把对象(不是指针)放到   容器将被销毁   自动(这很好)。

好坏取决于您系统的需求。这个实现意味着什么很简单:文档拥有Document s,当从文档中删除Paragraph时,任何指向它的指针都会立即变为无效。这意味着在实现以下内容时,您必须非常

  

除了使用集合之外的其他对象   段落,但不拥有它们。

现在你有问题了。您的对象Paragraph有一个指向ParagraphSelectionDialog所拥有的Paragraph个对象的指针列表。如果您不小心协调这两个对象,Document - 或通过Document的另一个客户端 - 可能会使Document实例持有的部分或全部指针无效!没有简单的方法可以捕获这个 - 指向有效ParagraphSelectionDialog的指针看起来与指向解除分配的Paragraph的指针相同,甚至可能最终指向有效 - 但不同 - {{1}实例!由于客户端被允许,甚至预期会保留和取消引用这些指针,Paragraph一旦从公共方法返回它们就会失去对它们的控制权,即使它保留了Paragraph个对象的所有权

这......很糟糕。你最终会得到一个不完整的,肤浅的,封装的,漏洞的抽象,并且在某些方面它比没有抽象更糟糕。因为隐藏了实现,所以客户端不知道接口所指向的对象的生命周期。大多数时候你可能会很幸运,因为大多数Document操作不会使对它们不修改的项的引用无效。一切都会很好......直到错误的Paragraph被删除,你发现自己仍然遇到了追踪调用堆栈的任务,寻找那个将指针保持太长时间的客户端。

修复很简单:返回值或对象,只要需要就可以存储,并在使用前进行验证。这可能是一个简单的序数或ID值,必须传递给std::list以换取可用的引用,或者像引用计数的智能指针或弱指针一样复杂...它真的取决于客户的具体需求。首先说明客户端代码,然后将Paragraph写入服务。

答案 5 :(得分:1)

简单方法

@cos,对于你所展示的例子,我想说用C ++创建这个系统最简单的方法就是不要引用引用计数。您所要做的就是确保程序流首先销毁对象(视图),该对象在根文档被销毁之前保存对集合中对象(段落)的直接引用。

艰难的方式

但是,如果您仍希望通过引用跟踪来控制生命周期,则可能必须将引用保持在层次结构的更深处,以便Paragraph对象保存对根Document对象的反向引用,这样,仅当最后一个段落对象被销毁时才会Document对象被破坏。

此外,段落引用在Views类中使用时,当传递给其他类时,也必须作为引用计数接口传递。

<强>韧性

与我在开头列出的简单方案相比,这是一个太多的开销。它避免了各种对象计数开销,更重要的是,继承您的程序的人不会陷入与您的系统交叉的引用依赖性线程陷阱中。

替代平台

这种工具可能更容易在支持和推广.NET或Java等编程风格的平台中执行。

您仍然需要担心内存

即使使用这样的平台,您仍然必须确保以适当的方式取消引用对象。其他杰出的参考文献可能会在眨眼间消耗掉你的记忆。所以你看,引用计数并不是良好的编程实践的灵丹妙药,尽管它有助于避免大量的错误检查和清理,这在应用时整个系统大大简化了程序员的任务。

<强>建议

那就是说,回到原来的问题,引起了所有引用计数的疑虑 - 是否可以直接从集合中公开你的对象?

程序不能存在于程序的所有类/所有部分彼此真正相互依赖的情况下。不,这是不可能的,因为程序是您的类/模块如何交互的运行表现。理想的设计只能最小化依赖关系,而不是完全删除它们。

所以我的观点是,是的,将您对集合中对象的引用暴露给需要使用它们的其他对象并不是一种不好的做法,只要您以理智的方式执行此操作

  1. 确保程序中只有少数几个类/部分可以获得此类引用,以确保最小的相互依赖性。

  2. 确保传递的引用/指针是接口而不是具体对象,以便在具体类之间避免相互依赖性。

  3. 确保引用不会进一步传递到程序中。

  4. 在清理满足这些引用的实际对象之前,请确保程序逻辑负责销毁依赖对象。

答案 6 :(得分:0)

我认为更大的问题是你隐藏了集合的类型,所以即使你使用不移动元素的集合,你可能会在未来改变主意。在外部,这是不可见的,所以我说这样做不是一个好主意。

答案 7 :(得分:0)

当你从列表中添加或删除东西时,

std :: list不会使任何迭代器,指针或引用无效(显然除了项目被移除的任何一点),因此以这种方式使用列表不会打破。

正如其他人所指出的那样,您可能不希望分发直接访问此类的私有位。所以将功能更改为:

  const MyContainedClass * getElement() const {
    // ...
    std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow
    return &(*it);
  }

可能更好,或者如果你总是返回一个有效的MyContainedClass对象,那么你可以使用

    const MyContainedClass& getElement() const {
    // ...
    std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow
    return *it;
  }

避免调用代码必须处理NULL指针。

答案 8 :(得分:0)

对于未来的程序员而言,STL比您的自定义封装更熟悉,因此如果可以,您应该避免这样做。有些边缘情况你还没有想到哪些会在应用程序的生命周期中出现,而STL则经过很好的审核和记录。

此外,大多数容器支持一些类似的操作,如begin end push等。因此,如果更改容器,更改代码中的容器类型应该是相当简单的。例如,矢量到deque或映射到hash_map等。

假设您仍然希望以更深层次的理由执行此操作,我会说正确的方法是实现列出实现的所有方法和迭代器类。无需更改时,将调用转发给成员列表调用。修改并转发或做一些自定义操作,你需要做一些特别的事情(这就是你首先决定这个的原因)

如果设计为继承的STl类会更容易,但为了提高效率,决定不这样做。谷歌“继承STL课程”以获得更多关于此的想法。

相关问题