进入智能指针,如何处理表示所有权?

Posted

技术标签:

【中文标题】进入智能指针,如何处理表示所有权?【英文标题】:Getting into smart pointers, how to deal with representing ownership? 【发布时间】:2019-04-18 17:53:04 【问题描述】:

我已经制作了一个动态图结构,其中节点和弧都是类(我的意思是弧是内存中的实际实例,它们不被节点到节点的邻接列表所暗示)。 每个节点都有一个指向它所连接的弧的指针列表。 每条弧都有 2 个指向它所连接的 2 个节点的指针。

删除一个节点会为它的每个弧调用 delete。 每个弧删除从它连接的 2 个节点的弧列表中删除其指针。 简化:

~node()
    
    while(arcs_list.size())
        
        delete arcs_list[arcs_list.size()-1];
        
    

~arc()
    
    node_from.remove_arc(this);
    node_to.remove_arc(this);
    

如果我想在这里开始使用智能指针,我该如何进行? 每个弧是否拥有 2 个节点,或者 2 个节点是否共享单个弧的所有权? 我在考虑一个 shared_ptr,但共享指针只会在两个节点都被删除时删除弧。如果我只删除一个节点,如果我使用 shared_ptr,我仍然必须显式删除它的所有弧。这完全违背了不使用原始指针的观点。

节点可以单独存在;每条弧由两个节点拥有,并且只有这两个节点都存在时它才能存在。

我应该使用其他类型的智能指针来处理这个问题吗? 还是原始指针只是简单的方法?

【问题讨论】:

“每个弧由两个节点拥有,并且只有这两个节点都存在时它才能存在” - 听起来节点共享弧的所有权并且应该将shared_ptrs 持有到它们拥有的弧. @JesperJuhl 但是从我所读到的关于 shared_ptr 如何工作的内容中,删除两个节点中的一个节点将允许两者之间的弧保持存在,因为弧只连接到一个节点,所以我会还是要手动删除弧线。 shared_ptrs 应该代表弧连接的两个节点,但我相信@Barnack 您还想将弧引用存储在节点中,所以我可以知道您为什么要复制相同的信息吗?跨度> 然后我猜想将弧引用作为原始指针存储在节点中,而无需操作它们,只需添加和删除引用并让弧 shared_ptrs 处理删除是您的方法。 @oekopez 我添加了一个 Graph 类;在添加和删除节点或弧时,用户仅与 Graph 类交互。 Graph 类在两个列表中和内部拥有 Arcs 和 Nodes,并负责在删除该节点时删除连接到该节点的所有弧。节点和弧保留彼此的原始原始指针以允许在图形中导航,但它们根本不负责内存管理。 【参考方案1】:

每条弧是否拥有 2 个节点,还是 2 个节点共享单个弧的所有权?

你自己回答了这个问题:

节点可以单独存在;每条弧由两个节点拥有,只有这两个节点都存在,它才能存在。

当对象A拥有对象B时,销毁B后对象A可以存在,但销毁A意味着销毁B。应用于您的情况,两个节点共享弧的所有权。

我应该使用其他类型的智能指针来处理这个问题吗?还是原始指针只是简单的方法?

啊,是的。这是真正的问题。对于这种情况,没有预制智能指针。但是,我不会在您的节点和/或弧类中使用原始指针。这意味着这些类需要在其主要目的之上实现内存管理。 (最好让每个班级做好一件事,然后尝试做很多事情并失败。)我看到了一些可行的选择。

1:编写自己的智能指针

编写一个可以封装必要的销毁逻辑的类。节点和/或弧类将使用您的新类而不是标准智能指针(而不是原始指针)。花一些时间来确保您的设计决策是可靠的。我猜你的新类需要某种功能/可调用来告诉它如何从它所在的列表中删除自己。或者可能将一些数据(如指向节点的指针)从 arc 类转移到新的类。

我还没有弄清楚细节,但这将是一种合理的方法,因为这种情况不适合任何标准的智能指针。关键是不要把这个逻辑直接放在你的节点和弧类中。

2:标记无效弧

如果您的程序不能立即释放内存,您可以采用不同的方法来解决弧删除问题。不要立即从其节点列表中删除弧,只需将弧标记为不再有效。当一个节点需要访问它的弧时,它(或者更好的是,它的列表)会检查它访问的每个弧——如果弧是无效的,那么它可以从列表中删除。从两个列表中删除节点后,正常的shared_ptr 功能将启动以删除弧对象。

这种方法的有用性会降低节点在其弧上迭代的频率。所以需要做出判断。

如何将弧标记为无效? 天真的方法是给它一个布尔标志。在构造函数中将标志设置为false,并在应该考虑删除弧时设置为true。有效,但确实需要一个新的领域。这可以在不使 arc 类膨胀的情况下完成吗?好吧,大概每个弧都需要指向其节点的指针。由于弧不拥有它的节点,这些可能是弱指针。因此,定义弧无效的一种方法是检查任一弱指针是否为expired()。 (请注意,当弧被直接删除时,弱指针可以手动reset(),而不是通过节点的删除。因此,过期的弱指针不一定意味着关联的节点已经消失,只是弧不再指向它。 )

在 arc 类相当大的情况下,您可能希望立即丢弃它的大部分内存,只留下一个存根。您可以添加一个间接级别来完成此操作。本质上,节点将共享一个指向唯一指针的指针,而唯一指针将指向您当前调用的弧类。当弧被删除时,唯一的指针是reset(),释放了弧的大部分内存。当此唯一指针为空时,弧无效。 (如果您可以接受将shared_ptr 存储到自身的对象,那么Davis Herring 的答案似乎是另一种以较少内存开销获得此效果的方法。)

3:使用 Boost.Bimap

如果您可以使用 Boost,他们有一个看起来可以解决您的问题的容器:Boost.Bimap。但是,你问,我不是已经使用邻接列表打折了吗?是的,但是这个 Bimap 不仅仅是一种将节点相互关联的方式。此容器支持将additional information 与每个关系关联。也就是说,Bimap 中的每个关系都将代表一个弧并且它会有一个与弧的信息相关联的对象。似乎很适合您的情况,并且您可以让其他人担心内存管理(总是一件好事,前提是您可以信任某人的能力)。

【讨论】:

【参考方案2】:

由于节点可以单独存在,它们属于图(可能是也可能不是单个对象),而不是弧(即使作为共享所有权)。正如您所观察到的,弧的节点对弧的所有权与通常的shared_ptr 情况是双重的,任何一个 所有者足以使对象保持活动状态。您仍然可以在此处使用shared_ptrweak_ptr(以及指向节点的原始、非拥有指针):

struct Node;
struct Arc 
  Node *a,*b;
private:
  std::shared_ptr<Arc> skyhookthis;
public:
  void free() skyhook.reset();
;
struct Node 
  std::vector<std::weak_ptr<Arc>> arcs;
  ~Node() 
    for(const auto &w : arcs)
      if(const auto a=w.lock()) a->free();
  
;

显然其他Node 操作必须检查空弱指针,并可能定期清除它们。

请注意,异常安全(包括 vs. bad_alloc 在构造 shared_ptr 时)需要 more care in constructing an Arc

【讨论】:

抱歉迟到的问题:Node::arcs 是否由 Arc 的 c'tor 填充,如下所示:a-&gt;arcs.push_back(skyhook);b-&gt;arc.push_back(skyhook);,除非为此使用Node::register(...) 之类的方法?跨度> @oekopez:是的,听起来不错,因为所有这些weak_ptrs 都必须直接或间接地派生自一个shared_ptr,而skyhook 当然是第一个创建的。跨度> 如果构造函数抛出,我刚刚在 Windows 上发现了这种方法的问题。不过,它可以在 Linux 上运行,请参阅:***.com/q/56750390/3032680 @oekopez:对于其他读者,请注意“在 Linux 上工作”只是此处未定义行为的反复无常。请参阅链接的问题。

以上是关于进入智能指针,如何处理表示所有权?的主要内容,如果未能解决你的问题,请参考以下文章

如何处理指针成员的不同所有权策略?

GTK+ 如何处理指针?

如何处理向量指针?

GRPC 如何处理出现多次的指针?

RTSP协议视频智能云平台EasyNVR从首页进入录像计划,出现报错“is not defined”如何处理?

如何处理指向通用接口的指针的 JPA 注释