当应该使用shared_ptr,而不是unique_ptr?

4

何时应该使用shared_ptr和unique_ptr?

例如,在这个类中,node* 应该改用shared_ptr或unique_ptr。这取决于什么?

class node
{
private:
    node *parent;
    vector<node*> children;

    /*
     * txny
     * x - numer drzewa
     * y - numer wezla
     */
    string id;
    typeNode type; //0 - term, 1 - func

public:
    node(node* parent, string id, typeNode type);
    virtual ~node() {}

    void addChild(node* child);
    void setChildren(vector<node*> &children);
    node* getChild(int i);
    int getChildrenNumber();
    void setParent(node* parent);
    node* getParent();
    string getId();
    typeNode getType();
    void setId(string id);
}; 

编辑:

类树拥有节点对象。我必须写更多的文字,因为无法保存更改。

class tree
{
private:
    vector <node*> nodes;
    int depth;
    int counterNodes;
    /*
     * pxty
     * x - numer populacji (generacji)
     * y - numer drzewa
     */
    string id;
private:
    node* root;
    node* parentGeneticPoint;
public:
    tree(int depth, string id);
    tree(int depth);
    ~tree();
public:
    void initialize(typeInit type);
    void showVector();
    void show();
    Mat run();
    tree* copy();
    tree* copySubtree(int subrootI);
    tree* copyWithoutSubtree(int subrootI);

};


1
谁拥有node对象?所有权语义通常指导使用哪种指针类型。 - Mark B
我编辑了。树拥有它。 - user3191398
3个回答

7
在这种树形结构的情况下,你应该拥有指向孩子节点的shared_ptr和指向父节点的weak_ptr
为什么?
- shared_ptr允许其他对象同样指向孩子节点(例如如果你在某处保存了一个子树的指针)。 - weak_ptr类似于shared_ptr但不拥有所有权。 因此,在这里,如果一个节点不再被其父节点或其他shared_ptr所引用,它将被销毁。 如果它自己的孩子节点具有shared_ptr,由于循环引用,该对象永远不会被销毁。
如果只有一个对象负责指针和它的删除,应使用unique_ptr。 这意味着特别地,你不应该复制指针。 但是你可以转移其所有权。
附加信息:
评论显示问题比这个答案更加复杂。 毕竟,整本书都专门讨论这个话题;-) 三个问题有助于您选择正确的设计:谁拥有指向的对象?指针将提供给外部世界吗?您要为这些指针提供什么保证?
如果你希望其他对象(即节点结构之外的对象)安全地指向你的节点(即在它们在外部使用时不希望该节点消失),你将需要shared_ptr。 当没有shared_ptr再引用一个节点时,该节点将被删除。
相反,如果节点“拥有”其孩子节点并且是唯一负责它们的销毁,则可能应该选择unique_ptrunique_ptr::get()可用于获取指向节点的(原始)指针,但无法保证它们将在以后的时间仍然有效。

5
如果父节点拥有子节点,应该选择使用 unique_ptr 而不是 shared_ptr。可以使用 std::unique_ptr::get() 共享指向 unique_ptr 子树的非所有者指针。 "唯一一个对象使用指针时应该使用 unique_ptr" 的说法是不正确的。只有一个对象应该 拥有 指针时才应该使用 unique_ptr。拥有和使用是非常不同的。 - anthonyvd
有道理,谢谢。如果我有另一个类,其中包含vector<tree*> forest,并且我使用unique_ptr,那么我可以拥有像void addTree(unique_ptr<tree>)和unique_ptr<tree> getTree()这样的函数吗? - user3191398
@AnthonyVallée-Dubois 我认为也不应该过于简化。unique_ptr::get()返回一个原始指针,因此即使子树指针仍可能需要它,您的节点也可以被删除。真正的问题是,您打算向外界提供哪些指针,并提供什么保证。我会进行编辑以澄清。 - Christophe
@AnthonyVallée-Dubois 如果一个资源有两个指针 - 第一个是shared_ptr,第二个是unique_ptr,那么这是正确的吗?“own”和"use”的区别是什么?在这种情况下,节点类拥有还是使用指向父级的指针? - user3191398

3

shared_ptr解决了内存管理问题,就像正则表达式解决html解析问题一样。

shared_ptr可以作为解决生命周期管理问题的一部分,但绝不能轻易使用。使用shared_ptr非常容易出现“放错”指针或引用循环。根据我的经验,应将其用作带有防护、不变量和公理的内部私有实现细节,这些证明您无法形成循环,并且有很好的机会避免问题。

我使用shared_ptr的超过一半是一个"拥有"指针的单个位置,其他观察者具有weak_ptr,除了在窄窗口期间检查资源仍然存在并且有理由认为shared_ptr不会在该窄窗口期死亡。

另一个使用大块的情况是当我有一个写时复制的情况,其中我有一个几乎不可变的对象状态,可以被复制(存储在shared_ptr<const T> pimpl中)。当发生写操作时,如果我是唯一的用户,我将其转换为shared_ptr<T>并修改它。否则,我将它复制到shared_ptr<T>中并修改它。然后在两种情况下都将其存储回shared_ptr<const T>

根据我的经验,简单地散布shared_ptr必然会导致泄漏和资源寿命过长。


另一方面,您应该只使用unique_ptrmake_uniqueunique_ptr应该在几乎所有情况下替换您代码中的newdelete。很难出错,而且当您出错时,通常是因为旧代码存在严重的泄漏风险,或者您不了解如何管理资源。

对于新代码,这是一个不用思考的选择。对于旧代码,如果需要了解涉及对象的生命周期,您可能仍然需要学习足够的知识将所有权放入unique_ptr中。唯一的例外是进行“ cargo cult”编程(修改您不理解的复杂系统以类似于系统中其他代码的方式,并希望它能够正常工作,因为其他代码已经工作),无法以这种方式管理资源。还有一些其他例外,例如以复杂方式管理其自身生命周期的对象。

使用unique_ptr管理非new分配的资源略微困难,但我也认为它是值得的。

有时候你不得不在长的C风格的void*调用链中释放指针。
使用shared_ptr有运行时开销,但是shared_ptr使对象生命周期更加难以理解的概念开销才是避免使用它的真正原因。 shared_ptr可以用于做一些花哨的事情,但是它不能解决你的问题,它是一个工具,作为全面资源管理系统的一部分,你可以使用它使你的解决方案变得更简单。 unique_ptr在运行时几乎没有任何开销:它与指针的大小相同,在生命周期结束时只需调用delete即可。 unique_ptr可以完全解决资源管理问题。 一旦你正确使用它们,这些问题就会消失。
在你的特定示例中,我建议将unique_ptr放入tree中,并在节点中保留原始指针。
或者,在children向量中保留unique_ptr,并在树和父指针中保留原始指针。
在任何一种情况下,所有添加/删除节点的操作都应该通过tree进行(以目标节点作为参数),因为必须保持treenode的状态同步。
当我指出在根tree的节点列表中保存节点是个坏主意时,你表现出了对获取随机节点的兴趣。
只需存储每个节点的子节点数量即可。 (这需要在添加/修改/删除必须级联到根的情况下进行工作)。
添加:
node* node::nth_node( int n ) {
  if (n == 0) return this;
  --n;
  for( auto&& child:children ) {
    if (n < child->subtree_size)
      return child->nth_node(n);
    n -= child->subtree_size;
  }
  return nullptr; // n is too big
}

这将获取节点的第n个后代,假设subtree_sizenode所在树的大小(包括它本身,因此它永远不应该为0)。
要从tree中获取随机节点,请创建一个从0root->subtree_size的随机数。例如,如果root->subtree_size3,则您的随机数为012
然后调用root->nth_node( that_random_number )

1
我假设你的父母拥有你的孩子。因此,父节点应该有一个指向孩子的唯一指针向量......孩子指向父节点的指针可以被消除,或者可以是原始指针(非所有权),只有在销毁孩子时才可能无效。另一方面,如果您希望树拥有节点,则它应该具有unique_ptr的向量,并且树节点中的指针本身应该是原始指针。任何一种方式都可以。 - Yakk - Adam Nevraumont
2
@user3191398,实际上,你的设计有点混乱:tree 中节点向量似乎并不是很有用。我会自己删除它,只留下每个节点拥有指向其子节点的 unique_ptr。在 tree 中只保留一个指向根节点的 unique_ptr。那些指针列表和树中的指针之间的耦合将增加复杂性并增加错误的可能性。 - Yakk - Adam Nevraumont
1
@user3191398 节点可以跟踪它们拥有的子节点数量。即使没有该向量,随机选择一个节点也非常快速:从0到根节点的子节点数(含)中选择一个随机数字。然后获取该节点(其中0是根节点)。 (假设树相对平衡,这是lg(n))。 - Yakk - Adam Nevraumont
1
@user319现在已经添加了一个描述,现在也可以这样做。 - Yakk - Adam Nevraumont
我可以再问你一个问题吗?如果我想要在树中更改一个节点,我应该将其作为引用还是值传递给函数?这样正确吗?http://www.wklej.org/id/1777112/ - user3191398
显示剩余4条评论

1
每当可能时,请使用unique_ptr(但要注意移动/放弃所有权),shared_ptr除了具有额外的内存和同步成本外,还有一个资源单一所有权的设计优势。

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