用于基于节点的树结构的Python绑定

时间:2018-07-07 15:36:22

标签: c++ polymorphism shared-ptr weak-ptr pybind11

上下文

我正在开发一个2D animation system,其中的数据被组织为基于节点的树结构,其语义和功能与XML DOM非常相似-但不够相似,仅使用现有的XML实现。为了清楚起见,让我们简化并假定数据结构是通过以下伪代码在概念上定义的:

class Node {
    Node* firstChild;
    Node* nextSibling;
    Node* parent;
}

遵循现代C ++(Core GuidelinesHerb Sutter talk等)的最新指南,适当的选择是使用智能指针来拥有句柄,而使用原始指针来获取非句柄。拥有手柄。由于所有权是唯一的,因此std::unique_ptr才有意义:

class Node {
    std::unique_ptr<Node> firstChild_;
    std::unique_ptr<Node> nextSibling_;
    Node* parent_;

public:
    Node() : parent_(nullptr) {}
    static std::unique_ptr<Node> make() { return std::make_unique<Node>(); }

    Node* firstChild() const { return firstChild_.get(); }
    Node* nextSibling() const { return nextSibling_.get(); }
    Node* parent() const { return parent_; }

    Node* appendChild(std::unique_ptr<Node> child) { ... }
    std::unique_ptr<Node> removeChild(Node* child) { ... }

    Node* makeChild() { return appendChild(make()); }
}

此API假定客户端代码知道它在做什么。就像使用list::iterator一样,客户端需要阅读文档以了解哪些操作可能会使节点无效,并且请注意不要盲目地将Node*保留为数据成员并在以后取消引用,因为晃来晃去。我支持C ++客户使用这种方法:我相信这是惯用的C ++,是您为性能付出的代价。

但是,我绝对需要为Python客户端提供更多的安全性。该动画软件是带有嵌入式Python控制台的C ++ GUI程序,这意味着许多Python客户端都是很少有编程经验的美术师,主要是从教程中复制粘贴代码并对其进行调整以适应他们的需求。更糟的是,当用户与可能有价值的未保存数据进行长期交互会话时,可能会执行这种不可靠的未经测试的Python代码。当Python客户端尝试使用过期的节点时,该软件绝对不应崩溃。预期的行为类似于:

[ Embedded Python Console ]
>>> node = getSelectedNode() # allocated from C++ and selected in the GUI
>>> print(node)
<Node at 0x14e4c30 with 42 child>
>>> node.parent
<Node at 0x14e4d24 with 12 child>
>>> deleteSelection() # or via GUI interaction
>>> print(node)
<Invalid node: the node has already been deleted>
>>> print(node.parent)
InvalidNodeError: the node has already been deleted
>>>

问题

您将如何使用pybind11包装此C ++ API以获得预期的行为?

如果有必要,您将如何更改C ++ API以允许这种行为,和/或使包装器代码更具可读性,习惯用法等?

我已经尝试过或考虑过的事情

天真地包装为class_<Node>(m, "Node")无效:Python实例无法知道该节点是否仍然有效。基本上,通过Node*返回take_ownership会导致双重删除,而通过referencereference_internal返回它们会导致未定义的行为(读取,分段错误)。

因此,我们要么需要一些PyNode蹦床类,就可以通过其他方式(例如,注册Node::onAboutToDie()之类的回调)来跟踪节点的有效性,或者需要更改C ++所有权模型以使用引用计数的smart指针。

我认为不错的选择是将C ++代码更改为使用shared_ptr而不是unique_ptr。我将有Nodeenable_shared_from_this派生出来,并将其包装为class_<Node, PyWeakPtr<Node>>(m, "Node"),其中PyWeakPtr是一个自定义持有人,在引擎盖下存储着weak_ptr。由于节点是从enable_shared_from_this派生的,因此持有人可以使用PyWeakPtr(T*)构造函数将原始指针转换为weak_ptr。简而言之,这将利用共享指针的引用计数功能不具有共享所有权(所有权仍然是唯一的),而只是能够在Python端使用弱引用,该引用将在每次使用前检查expired()访问,如果过期则抛出可捕获的异常。但是,到目前为止,我的各种尝试都失败了:要么我无法获得预期的行为,要么甚至无法对其进行编译,等等。

也基于shared_ptr的一个更明显的解决方案是使用class_<Node, std::shared_ptr<Node>>(m, "Node")代替我们的自定义持有人PyWeakPtr,但是它也不起作用,因为它会人为地延长节点的生存期,因为只要某些python变量指向它们即可。我认为这是一个泄漏:不应共享节点,并且如果用户在UI中删除节点,则该节点应消失并且应立即调用析构函数。语义上确实应该是,在Python中访问“被语义删除”的节点会引发Python错误(而不会导致C ++程序崩溃),而不是悄悄地伪装成有效节点。

如果有用的话,我可能会花一些时间来清理/ make_minimal / etc这样的尝试,但是为了简洁起见,我现在将问题留在原地,也许这里的一些专家已经有了一些有用的见解,例如是否整个方法甚至都有道理,等等:)

请注意,许多解决类似问题的现有软件,例如Qt的QDomDocument或Pixar的Universal Scene Description甚至在C ++ API中都倾向于使用引用计数的弱引用(或多或少隐藏在内部),从而完全避免了错误的C ++和Python客户端。我也对这种方法持开放态度,尽管理想情况下,我认为我更喜欢坚持在C ++中简单使用非所有者原始指针的一般准则。

0 个答案:

没有答案