3
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析构函数中的元素),共享指针集合(我不喜欢)等。

4

9 回答 9

1

好好想想真正想要什么MyClass。我注意到一些程序员为他们的集合编写包装器只是出于一种习惯,不管他们是否有超出标准 STL 集合所满足的任何特定需求。如果这是你的情况,那么typedef std::list<MyContainedClass> MyClass就完成它。

如果您确实有打算在 中实现的操作MyClass,那么封装的成功将更多地取决于您为它们提供的接口,而不是您如何提供对底层列表的访问。

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


@cos

当然,我封装 MyContainedClass 不仅仅是为了封装。让我们举个更具体的例子:

您的示例并没有减轻我对您在知道容器将用于什么之前编写容器的恐惧。您的示例容器包装器Document共有三个方法:NewParagraph()DeleteParagraph()GetParagraph(),所有这些方法都对包含的集合 ( std::list) 进行操作,并且所有这些方法都std::list与提供“开箱即用”的操作密切相关。Document从某种意义上封装 std::list ,客户端不需要知道它在实现中的使用......但实际上,它只不过是一个门面 - 因为您正在向客户端提供指向存储在列表中的对象的原始指针,所以客户端仍然隐式绑定到实现。

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

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

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

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

这是不好的。你最终会得到一个不完整的、肤浅的、封装、泄漏的抽象,在某些方面它比根本没有抽象还要糟糕。因为你隐藏了实现,你的客户不知道你的接口指向的对象的生命周期。大多数时候您可能会很幸运,因为大多数std::list操作不会使对它们未修改的项目的引用无效。一切都会好起来的......直到错误Paragraph被删除,您发现自己被困在通过调用堆栈进行跟踪的任务中,以寻找使该指针保持太长的客户端。

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

于 2008-09-20T17:20:51.537 回答
1

我看不到封装它的用途,但这可能只是我。无论如何,返回引用而不是指针对我来说更有意义。

于 2008-09-20T15:16:29.103 回答
1

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

因此,MyClass 应该提供方法来操作包含的类对象,而不是返回指向它们的指针。因此,例如,诸如“增加第无数个包含对象的值”之类的方法,而不是“这里是指向第无数个包含对象的指针,随心所欲地使用它”。

于 2008-09-20T15:20:02.233 回答
1

这取决于...

这取决于您希望您的类封装多少,以及您想要隐藏或显示什么。

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

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

这是你的选择,我猜。

请注意,如果它 == std::list<>.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,所以......

于 2008-09-20T15:23:04.970 回答
1

Scott Meyers 在他的《Effective STL: 50 Specific Ways to Improvement Your Use of the Standard Template Library 》一书中说,尝试封装你的容器是不值得的,因为它们都不能完全替代另一个容器。

于 2008-09-20T15:24:31.847 回答
1

简单的方法

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

艰难的道路

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

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

韧性

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

替代平台

这种工具可能更容易在支持和促进这种编程风格的平台(如 .NET 或 Java)中执行。

你仍然需要担心内存

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

推荐

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

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

所以我的意见是,是的,将集合中对象的引用暴露给需要使用它们的其他对象并不是一个坏习惯,前提是您以理智的方式执行此操作

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

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

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

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

于 2008-09-23T06:52:01.087 回答
0

未来的程序员会比您的自定义封装更熟悉 STL,因此您应该尽可能避免这样做。在应用程序生命周期的后期会出现您没有想到的边缘情况,而 STL 是失败的,并且经过了很好的审查和记录。

此外,大多数容器都支持一些类似的操作,例如开始结束推送等。因此,如果您更改容器,则在代码中更改容器类型应该是相当简单的。例如向量到双端队列或映射到 hash_map 等。

假设您出于更深层次的原因仍想这样做,我想说正确的方法是实现 list 实现的所有方法和迭代器类。当您不需要更改时,将呼叫转发到成员列表呼叫。修改和转发或在您需要做一些特殊的地方做一些自定义操作(您首先决定这样做的原因)

如果 STl 类设计为继承自它会更容易,但出于效率考虑,决定不这样做。谷歌“从 STL 类继承”以获得更多关于此的想法。

于 2008-09-20T16:37:15.307 回答
0

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

于 2008-09-20T15:02:27.767 回答
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 指针。

于 2008-09-20T15:32:09.893 回答