C++:用共享智能指针和弱引用指针替代裸指针

4

我在程序中遇到了一个设计问题。我需要管理节点对象,这些节点对象是根ChainDescriptor的一部分。

基本上看起来像下面这样:

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<shared_ptr<...> >
  • 使用weak_ptr和相应的list<weak_ptr<...> >
在上面的示例中,我确实不知道在哪里正确使用shared_ptrweak_ptr。有什么建议吗?

那么boost::ptr_list呢? - Bartek Banachewicz
4个回答

3

shared_ptr是拥有智能指针,而weak_ptr是引用智能指针。

因此,在您的情况下,我认为ChainDescriptor应该使用shared_ptr(它拥有节点),而Node应该对m_parent使用weak_ptr(它只是引用它),并对m_childs使用shared_ptr(它拥有它们)。


你能解释一下 weak_ptr 相对于原始指针在父对象中的优势吗? - James Kanze
1
使用weak_ptr是安全的。原始指针的有效性必须通过不变量来确保(如Sergey的答案所述)。weak_ptr的主要问题是其性能影响-如果需要速度,没有什么可以与原始指针竞争。 - Johny
使用weak_ptr很复杂,我不认为它真正增加了任何安全性。这不仅是速度的问题,也是易用性的问题。如果weak_ptr可以防止程序员可能犯的某些错误,那就另当别论了。但在这种情况下,他的代码只支持单个父级,并且删除父级将删除子级,因此一旦删除父级,指向父级的指针就不可能被访问。 - James Kanze

3
你可以使用shared_ptr来管理m_childs,并使用weak_ptr来管理m_parent
但是,保留指向父节点Node的裸指针并且不使用任何弱指针仍然是合理的选择。背后的保护机制是非空父节点始终存在的不变量。
另一种选择是仅在ChainDescriptor中使用shared_ptr,并保留Node中所有裸指针。这种方法避免了使用弱指针,并具有清晰的所有权策略(父节点拥有其子节点)。
弱指针可以帮助你自动处理内存,但其缺点是模糊的所有权逻辑和性能惩罚。

2
+1 提到原始指针选项。我认为它们是表达“m_parent”的所有权情况的正确工具。 - risingDarkness
@Sergey,我同意。 我开始朝着这个方向编写原型。 ChainDescriptor将包含shared_ptr列表。对于Node类,我开始编写weak_ptr<Node>列表,但在列表中搜索节点有点棘手,因为weak_ptr中没有定义==运算符。 - Zyend
你是否很少通过weak_ptr访问父级对象?如果是这样,那么weak_ptr将成为你最好的朋友。如果你需要一直访问父级对象,比如在内部循环中,考虑使用裸指针。它们并不是坏东西。 - Sergey K.
@Sergey,实际上每个节点可能有多个父节点(它是一棵树)。该链必须能够随时访问特定节点的“上游”节点。因此,每个节点都有两个列表:上游节点列表和子节点列表。但是,如果我使用shared_ptr列表来处理上游节点,就会出现循环问题和内存泄漏。因此,我担心必须处理不同类型的列表。 子节点为list<shared_ptr>,上游节点则为list<weak_ptr>或list<rawptr>? - Zyend
1
如果你确定结构是持久的,并且父对象在子对象之后被销毁(最好是这样),那么你可以愉快地使用裸指针。相信我。 - Sergey K.

1
通常的实现方式是每个节点都有对其子节点的强引用(即保持它们的存活状态),而每个子节点则会有一个指向父节点的弱引用。
这样做的原因是为了避免循环引用。如果只使用强引用,那么就会出现这样一种情况:父节点的引用计数永远不会降至零(因为子节点有一个引用),而子节点的引用计数也永远不会降至零(因为父节点有一个引用)。
我认为你的 ChainDescriptor 类在这里可以使用强引用。

通常的实现会使用原始指针作为指向父级的后向指针。这里不需要弱指针,因为所有权语义保证子对象不能比其父对象存在更久的时间。根据结构的不同,如果是有向无环图,则必须共享子指针,但否则,unique_ptr同样可以胜任此工作,并且复杂度和开销都要小得多。 - James Kanze
真的... 我的想法是,如果在某个时候需要一个 Node::getParent() 函数,那么你会希望它返回一个完整的 shared_ptr,这样升级持有的 weak_ptr 比使用 enable_shared_from_this 更容易。 - Tristan Brindle
@Tristan,好的我同意。ChainDescriptor包含了整个节点列表。但是如果ChainDescriptor删除了一个节点(即chainDesc.removeNode("blabla")),它将从主列表中删除该节点,并且也从所有以“ blabla”为子节点的其他节点中删除。 - Zyend

1
尝试仅用某种智能指针替换原始指针通常不起作用。智能指针与弱指针具有不同的语义,通常需要在更高层次上考虑这些特殊语义。在这里最“干净”的解决方案是在ChainDescriptor中添加对拷贝的支持,实现深度拷贝。(我假设你可以克隆节点,并且所有的节点都由ChainDescriptor拥有。)此外,对于撤销,您可能仍需要深度复制;您不希望活动实例中的修改修改为撤销保存的数据。
说到这里,你的节点似乎被用来构建一棵树。在这种情况下,只要 1) 所有的Node都始终由ChainDescriptor或父节点所拥有,并且2) 结构确实是一个森林,或者至少是一组DAG(当然,您不会更改任何已保存的实例),那么std::shared_ptr就可以工作。如果结构可能出现循环,则不能在此级别使用shared_ptr。您可以将节点列表和树抽象成一个单独的实现类,并让ChainDescriptor保持对其的shared_ptr。(FWIW:我在多年前编写的解析树中为节点使用了引用计数指针,不同的实例可以共享子树。但我从一开始就设计使用引用计数指针。由于树的构造方式,我可以保证不会出现循环。)

是的,节点始终由链或父节点拥有。但是如果节点从链中删除(即 chain.removeNode("blabla")),则机制也应将其从拥有它的任何节点的子列表中删除。 - Zyend
@Zyend 我不知道有任何智能指针可以做到这一点。你的意思是你必须导航到父节点,并从那里删除节点吗? - James Kanze
@Zyend,另外:您实际上将什么放入撤消列表中?当您对原始对象进行修改时,您希望它如何工作?从您的描述中,我非常强烈地感觉到您需要深度复制。(假设这些对象目前无法复制。) - James Kanze

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接