如何在 C++ 中实现这个数据结构

Posted

技术标签:

【中文标题】如何在 C++ 中实现这个数据结构【英文标题】:How to implement this data structure in c++ 【发布时间】:2019-03-01 21:31:51 【问题描述】:

我正在从 C#/Java 迁移到 C++,并且无法正确地了解做事的方法。

我正在构建一种数据结构来支持算法和功能。所以假设一个外部对象拥有一个std::vector<Widget> vecW 和一个std::vector<Vidget> vecV。它创建了许多Widget 类型的对象,并将它们添加到vecW。它随后创建了许多Vidget 的实例,并将它们添加到vecV。 (以防万一:当外部对象被销毁时,应该释放为这些对象分配的内存。)

现在,有一些自定义逻辑规定,对于列表中的每个 Widget 类型的对象,它需要访问一些 Vidget 类型的对象。 (反之亦然,如果小部件w 可以访问小部件v,那么小部件v 需要访问小部件w。)

在 C# 中,我会在每个 Widget 中保留一个 List<vidget> ListOfVidgets,在每个 Vidget 中保留一个 List<Widget> ListOfWidgets,并根据自定义逻辑实例化这些列表。 Java 类似(我相信 C# 中的 List<Vidget> 类似于 Java 中的 arraylist<Vidget>,有点像 C++ 中的 std::vector<Vidget*>/std::vector<Vidget>)。

所以我可以在每个小部件中使用std::vector<Vidget*>,以便根据自定义逻辑进行实例化。这是最好的方法吗?或者是否有优先使用智能指针(甚至其他方法)的方法?


编辑:生命周期如下: 1) 外部对象 get 的创建和填充(带有小部件/vidgets)。 2)自定义逻辑确定关系。 3) 使用数据结构。 (因此在使用期间不会更改关系和/或添加/删除小部件。) 4)(仅)当外部对象被销毁时,需要释放内存。

【问题讨论】:

评论不用于扩展讨论;这个对话是moved to chat。 【参考方案1】:

这就是我要做的。

class Widget 
  ...
    void AddVidget(Vidget* vidget);
  private:
    std::vector<Vidget*> vigets_;
;
std::vector<std::unique_ptr<Vidget>> vidgets;
// Since widgets will have references to vidgets, safet
// to instantiate after vidgets (so widgets is cleaned up
// first).
std::vector<std::unique_ptr<Widget>> widgets;
...
widgets[i].add_vidget(vidjets[j].get());

这使您的指针稳定。

【讨论】:

这种方法的缺点是,在外部对象允许销毁内部对象后,无法明智地处理内部对象尝试访问另一个内部对象的情况。但这可能是最简单的方法。 我的评论是我不认为这反映了 OP。但是,重读它,我认为您没有正确处理 unique_ptr。真的没有办法正确处理 unique_ptr 并且在其他地方有额外的指针副本。 所以我喜欢这个,并且似乎理解。它与其他算法中的想法很接近。但是互联网上(和书中)的每个人都告诉我要远离“原始”指针。那么这不适用于这里吗? @willem 有很多不好的/相互矛盾的建议。我强烈建议通读 Stroustrup & Sutter github.com/isocpp/CppCoreGuidelines/blob/master/… 发布的这些指南,只要它们不是拥有的,原始指针就可以。 @willlem Gaik 是完全正确的。基本上,通过接受原始指针,它在 API 中向调用者暗示调用者必须确保指针的生命周期长于小部件的生命周期。这可能很微妙(例如,我只是修复了一件事!)但通常不会容易出错。 shared_ptr 通常同样容易出错(循环共享依赖创建不朽的对象)并且 shared_ptr 很慢,所以我倾向于说“仅在确实需要时才使用共享指针”。【参考方案2】:

使用std::vector 的主要问题是,当向量发生变化时,所包含对象的地址会发生变化——尤其是在你填充它的时候。

鉴于您的生命周期特征,您的对象将在任何关系建立之前完全创建,并且在处理结束之前不会更改。这意味着使用关系指针不会有问题。

在这种情况下,我可能会这样做:

class Widget

public:

private:
    std::vector<class Vidget*> ListOfVidgets;
;

class Vidget

public:

private:    
    std::vector<class Widget*> ListOfWidgets;
;

class OuterObject

public:

private:
    std::vector<Widget> vecW;
    std::vector<Vidget> vecV;
;

这应该是非常有效的,因为所有权语义已经确立。外层向量拥有一切,而内层对象向量只包含非拥有关系指针。

【讨论】:

一般来说,“非拥有关系指针”是对原始指针的好用吗? @willem 是的。它基本上是现代 C++ 代码中原始指针的优点。所有拥有的对象都应该在容器中或由智能指针管理。唯一需要注意的是当您将对象存储在向量中时它们会移动。在您的设置中,这不是问题。但如果是,您可以将对象的外部列表移至 std::list 而不是 std::vector 好的,这说明了很多。谢谢【参考方案3】:

一般来说,我会首先寻找一种重新设计的方法,以便消除此要求。对象通常不需要以这种方式通过它们上方的对象找到其他对象。有时您可以安排在每次执行对象时通过其所有者传递对象信息。

但假设您无法更改设计,您就不想使用指针或引用。您不想使用它们,因为向量可以在内存中移动它们的对象,从而使指针和引用都无效。此外,普通指针会使所有权不清楚。

相反,对对象使用std::shared_ptrs 的向量。如果可能,使用std::make_shared 创建对象。这将管理对象的生命周期,直到它从向量中删除。

对于一个需要访问另一个对象的对象,您有两种选择。如果您可能希望延长被引用对象的生命周期,直到持有对它的引用的对象被销毁,请对同一对象使用另一个 std::shared_ptr

如果您不想延长对象的生命周期,请使用std::weak_ptr - 请记住,如果目标的生命周期结束,弱指针可能会变得陈旧。您要么需要确保这种情况永远不会发生,要么在每次访问时检查弱指针。无论如何,最好总是检查它,以便妥善处理错误情况。

按照我个人的喜好,你可以:

    重新架构,以便无需存储这些引用。例如,外部对象可以拥有关系并在每次调用内部对象时传递对内部对象的引用。甚至可能有更好的选择。外部对象可以提供内部对象可以使用的 API,内部对象可以保持对外部对象的引用。

    使用shared_ptrweak_ptr。这很干净,易于理解,并且可以干净地捕获任何错误。它有一些开销,但仅限于设置和拆卸,如果这些对象是长期存在的,这无关紧要。

    使用自定义智能指针。外部对象可以为内部对象提供自定义智能指针,从而为内部对象提供访问所需对象的简便方法。

    使用unique_ptr 和原始指针向量。

【讨论】:

【参考方案4】:

不要乱用指针。那是旧的 C 风格的思维方式。

如果您要使用 C++ 中的对象,请创建 WidgetVidget 的列表/向量/映射,并让 STL 容器拥有相关对象。那么您就不必担心取消引用错误指针等问题了。

至于您的下一个要求,即Widgets 需要访问Vidgets,反之亦然,您需要在映射中实现该逻辑。我认为就数据库而言,所以我有一个映射 W-to-V 和 V-to-W 的表,其方法会将 references 返回到必要的对象。当涉及到 C++ 时,我可能会使用带有某种 ID(如果需要的话是人工的)的 std::map 来实现它到小部件或 vidget。

【讨论】:

引用有一些优点,但它们仍然可以失效 我认为智能指针实际上是这里的解决方案。而且你不能存储引用,因为对象会移动(在重定位时的向量内,或者当向量被传递时)。 问题是你不能只让容器管理生命周期,因为在这种情况下所有权的划分没有很好地建立。这是一种从不存在这些问题的语言移植而来的算法,共享所有权是常态。 @FrançoisAndrieux 我相信外部容器是所有对象的所有者。他们一直活着,直到它被摧毁。 @FrançoisAndrieux;是的,有一个外部对象,它拥有一切。它也许应该在这个问题中发挥更突出的作用;我不确定这是否重要

以上是关于如何在 C++ 中实现这个数据结构的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C++ 中实现观察者设计模式流数据?

如何在 C++ 中实现这个结果?指向数组的数组

如何在qt中实现代码完成[关闭]

在 C++ 中实现深度优先搜索

在 C++ 中实现多个接口

在非托管 C++ 程序中实现 C# DLL COM 文件