使用unique_ptr指示(非)所有权转移

3

假设我有一个这样的类:

class Node {
public:
    Node(Node* parent = 0) : mParent(parent) {}
    virtual ~Node() {
        for(auto p : mChildren) delete p;
    }

    // Takes ownership
    void addChild(Node* n);

    // Returns object with ownership
    Node* firstChild() const;

    // Does not take ownership
    void setParent(Node* n) { mParent = n; }

    // Returns parent, does not transfer ownership
    Node* parent() const { return mParent; }

private:
list<Node*> mChildren;
Node* mParent;
};

我现在想使用智能指针和/或右值引用来表明所有权何时转移。

我的第一个猜测是将mChildren更改为包含unique_ptr,并相应地调整函数签名。

    // Takes ownership
    void addChild(unique_ptr<Node> n);

    // Returns object with ownership
    unique_ptr<Node>& firstChild() const;

    // Does not take ownership
    void setParent(Node* n) { mParent = n; }

    // Returns parent, does not transfer ownership
    Node* parent() const { return mParent; }

现在,当我需要将Node::firstChild()的结果传递给一些观察它但不拥有它的函数时,这可能会有些棘手,因为我需要在unique_ptr上显式调用.get(),而根据我的理解,这并不推荐。
有没有更好、更推荐的方法来使用unique_ptr表示所有权,而无需使用.get()和传递裸指针?

为什么不将其作为对const unique_ptr的引用传递给函数?这样,函数就可以查看它,而不会复制它。 - JBL
@JBL 因为例如 parent() 返回一个非唯一指针,这意味着我必须为原始指针和唯一指针重载每个函数。 - Symaxion
1个回答

8
首先,我会使用 std::vector 而不是 std::list 来包含子节点。除非您有强烈的理由不使用它,否则 std::vector 应该是默认容器。如果您担心性能问题,请放心,因为 std::vector 执行的连续分配很可能会导致更高的缓存命中率,从而大大加快访问速度,相对于 std::list,后者意味着散乱的分配/访问模式。
其次,你正确地使用了 std::vector<std::unique_ptr<Node>> 来持有子节点,因为可以合理地假设一个节点拥有其子节点的所有权。另一方面,除了由 addChild() 接受的指针之外,所有其他指针都应该是非拥有的原始指针。
这适用于 mParent 指针和由 Node 的成员函数返回的指针。实际上,firstChild() 成员函数甚至可以返回一个引用,如果节点没有子节点,则抛出异常。这样,您就不会产生关于谁拥有所返回对象的任何混淆。
返回一个 unique_ptrunique_ptr 的引用不是正确的习惯用法: unique pointers 表示所有权,您不希望将所有权交给 Node 的客户端。
以下是您的类的示例代码:
#include <vector>
#include <memory>
#include <stdexcept>

class Node {
public:
    Node() : mParent(nullptr) { }

    void addChild(std::unique_ptr<Node>&& ptr) {
        mChildren.push_back(std::move(ptr));
        ptr->setParent(this);
    }

    Node& firstChild() const {
        if (mChildren.size() == 0) { throw std::logic_error("No children"); }
        else return *(mChildren[0].get());
    }

    Node& parent() const {
        if (mParent == nullptr) { throw std::logic_error("No parent"); }
        else return *mParent;
    }

private:

    void setParent(Node* n) { 
        mParent = n; 
    }

    std::vector<std::unique_ptr<Node>> mChildren;
    Node* mParent;
};

如果想要避免抛出异常,您当然可以决定返回非拥有,可能为空的裸指针而不是引用。或者您可以添加一对hasParent()getNumOfChildren()方法来检索Node状态的信息。这将允许客户端在不想处理异常的情况下执行检查。


我可能需要进行许多中间插入/删除操作,这就是为什么我最初选择std::list而不是std::vector的原因。 - Symaxion
@SeySayux: std::vector可能仍然更优秀,因为具有更高的缓存命中率(尽管复杂度更差,在实践中,内存访问模式占主导地位)。在承诺使用std::list之前,请尝试进行一些测量。 - Andy Prowl
unique_ptr通过右值引用而不是值传递获取,这样做是否必要/有优势? - dyp
@DyP:这是一种风格问题。在这种情况下并非必需,因为客户端总是需要执行std::move(),但是通常需要明确表达您希望客户端提供一个rvalue(如果他们想要传递lvalue,则必须使用std::move())。因此,即使对于像std::unique_ptr这样的不可复制类,我也会这样做,因为客户端只能提供rvalue,这并不严格必要。 - Andy Prowl
@SeySayux 你会从容器中间插入和删除比迭代容器更频繁吗?我怀疑不会。c.erase(std::remove_if(c.begin(), c.end(), [&]()->bool{ code }), c.end());可以提供O(n)的迭代和删除元素,类似的技巧也可以用于在容器中间插入批量数据。 - Yakk - Adam Nevraumont

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