【问题标题】:C++: Replace raw pointers with shared and weak ptrC++:用共享和弱 ptr 替换原始指针
【发布时间】:2013-11-27 07:52:34
【问题描述】:

我的程序遇到了设计问题。 我必须管理属于根 ChainDescriptor 的 Nodes 对象。

基本上如下所示:

class ChainDescriptor
{
public:
    ~ChainDescriptor()
    {
        //delete the nodes in nodes...
    }

    void addNode(Node *);
    Node * getNode();

    const std::list<Node *>& getNodes() const;

    std::list<Node *> m_nodes;

};

class Node
{
public:
    Node(Node *parent);

    void addChild(Node *node);
    Node * getChild(const std::string& nodeName);

private:
    Node * m_parent;
    std::list<Node*> m_childs;
};

ChainDescriptor 类拥有所有节点并负责删除它们。 但是这些类现在需要在另一个程序中使用,一个具有撤消/重做功能的 GUI,具有“所有权”的问题。 在深入修改现有代码之前,我正在考虑不同的解决方案:

  • 使用shared_ptr 和各自的list&lt;shared_ptr&lt;...&gt; &gt;
  • 使用weak_ptr 和各自的list&lt;weak_ptr&lt;...&gt; &gt;

在上面的例子中,我真的不知道在哪里正确使用shared_ptrweak_ptr

有什么建议吗?

【问题讨论】:

标签: c++ memory-management boost shared-ptr weak-ptr


【解决方案1】:

试图用某种智能替换原始指针 指针一般不起作用。智能指针有 与弱指针不同的语义,通常,这些 特殊语义需要在更高层次上考虑 等级。这里“最干净”的解决方案是添加对复制的支持 在ChainDescriptor,实现深拷贝。 (我想 在这里你可以克隆Node,并且所有Node都是 始终由 ChainDescriptor 拥有。)此外,对于撤消,您可以 无论如何都需要深拷贝;你不想修改 活动实例来修改为撤消保存的数据。

话虽如此,您的节点似乎被用来形成一棵树。在 在这种情况下,std::shared_ptr 将起作用,只要 1) 所有Node 始终由ChainDescriptor 或父母“拥有” Node,和 2) 这个结构真的是一片森林,或者至少 DAG 的集合(当然,您没有进行更改 在任何已保存的实例中)。如果结构是这样的 可能会出现循环,那么此时您不能使用shared_ptr 等级。您也许可以抽象出节点列表和 树成一个单独的实现类,并有 ChainDescriptor 保留 shared_ptr 到此。

(FWIW:我为 我多年前写的解析树,以及不同的实例 可以共享子树。但我从 开始使用引用计数指针。而且由于如何 树建好了,我保证不会有 循环。)

【讨论】:

  • 是的,一个节点总是被链或父节点所依赖。但是如果一个节点从链中删除(即chain.removeNode(“blabla”),那么该机制也应该从拥有它的任何节点的子列表中删除它。
  • @Zyend 我不知道有什么智能指针可以做到这一点。你的意思是你必须导航到父节点,并从中删除节点?
  • @Zyend 另外:您实际上在撤消列表中放入了什么,当您对原始对象进行修改时,您希望它如何工作?从您的描述中,我得到一个非常强烈的印象,即您需要深拷贝。 (据推测,这些对象当前不可复制。)
【解决方案2】:

通常的实现是每个节点对其子节点具有强引用(即保持它们处于活动状态),并且每个子节点对父节点具有弱引用。

这样做的原因是为了避免循环引用。如果只使用强引用,那么您将遇到这样一种情况:父引用计数永远不会降为零(因为子引用有引用),而子引用计数永远不会降为零(因为父引用有引用)。

我认为您的 ChainDescriptor 类可以在这里使用强引用。

【讨论】:

  • 通常的实现将使用原始指针作为指向父级的反向指针。这里不需要弱指针,因为所有权语义保证孩子不能活得比它的父母长。并且取决于结构:如果它是 DAG,则必须共享子指针,否则,unique_ptr 也可以完成这项工作,而且复杂性和开销要少得多。
  • 是的...我的想法是,如果在某个时候需要Node::getParent() 函数,那么您希望它返回一个完整的shared_ptr,在这种情况下它会是比使用enable_shared_from_this 更容易“升级”持有的weak_ptr
  • @Tristan,我当然同意。 ChainDescriptor 包含整个节点列表。但是,如果 ChainDescriptor 删除了一个节点(即 chainDesc.removeNode("blabla") ),它将从主列表中删除它,但也会从所有其他以“blabla”为子节点的节点中删除。
【解决方案3】:

您可以将shared_ptr 用于m_childs,将weak_ptr 用于m_parent

但是,保留指向父 Node 的原始指针并且根本不使用任何弱指针可能仍然是合理的。这背后的保障机制是non-null parent总是存在的不变量。

另一种选择是仅在ChainDescriptor 中使用shared_ptr,并在Node 中保留所有原始指针。这种方法避免了弱指针并且拥有清晰的所有权策略(父节点拥有他们的子节点)。

弱指针将帮助您自动管理内存,但其背后是模糊的所有权逻辑和性能损失。

【讨论】:

  • +1 用于提及原始指针的选项。我认为它们是表达 m_parent 所有权情况的正确工具。
  • @Sergey,我同意。我开始朝这个方向写一个原型。 ChainDescriptor 将包含 shared_ptr 列表。对于 Node 类,我开始编写 weak_ptr 列表,但是在列表中搜索节点有点棘手,因为在 weak_ptr 中没有定义 == 运算符。
  • 您很少通过weak_ptr 访问父级吗?如果是这样,weak_ptr 是你最好的朋友。如果您需要一直访问父级,在您的内部循环中的某个地方,请考虑原始指针。他们还不错。
  • @Sergey,每个节点实际上可能有几个父节点(它是一棵树)。链必须能够随时访问特定节点的“上游”节点。因此,每个节点都有 2 个列表:上游节点列表和子节点。但是如果我对上游节点使用 shared_ptr 列表,我会遇到循环问题和内存泄漏。因此,恐怕我必须处理不同类型的列表。 list 用于子节点, list 或 list 用于上游节点?
  • 如果你确定结构是持久的并且父母在孩子之后被摧毁(你最好是这样),你可以愉快地使用原始指针。相信我。
【解决方案4】:

shared_ptr 拥有智能指针,weak_ptr 正在引用智能指针。

所以在你的情况下,我认为ChainDescriptor 应该使用shared_ptr(它拥有节点),Node 应该使用weak_ptr 来表示m_parent(它只引用它)和shared_ptr 来表示@987654329 @(它拥有它们)。

【讨论】:

  • 您能解释一下weak_ptr 相对于父级原始指针的优势吗?
  • weak_ptr 的使用是安全的。原始指针的有效性必须通过不变量来确保(如 Sergey 的回答中所述)。 weak_ptr 的主要问题是它对性能的影响——如果你需要速度,没有什么能与原始指针竞争。
  • weak_ptr 的使用很复杂,我看不出它在哪里真正增加了安全性。这不仅仅是速度的问题,也是易用性的问题。如果weak_ptr 防止程序员可能实际犯的某种错误,那将是一回事。但是在这种情况下,他的代码只支持单父,删除父会删除子,所以一旦删除了父,就不可能访问到父指针。
猜你喜欢
  • 1970-01-01
  • 2012-01-11
  • 1970-01-01
  • 2014-06-25
  • 1970-01-01
  • 1970-01-01
  • 2020-04-28
  • 1970-01-01
相关资源
最近更新 更多