语境
我正在开发一个2D 动画系统,其中数据组织为基于节点的树结构,其语义和功能与XML DOM非常相似——但不太相似,无法仅使用现有的 XML 实现。为了清楚起见,让我们简化并假设数据结构是通过以下伪代码在概念上定义的:
class Node {
Node* firstChild;
Node* nextSibling;
Node* parent;
}
遵循现代 C++ 的最新指南(Core Guidelines、Herb 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 假定客户端代码知道它在做什么。就像使用 a 时一样list::iterator
,客户端需要阅读文档以了解哪些操作可能会使节点无效,并注意不要盲目地将 aNode*
作为数据成员并在以后取消引用它,因为它可能是悬空的。对于 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 以允许此类行为,和/或使包装器代码更具可读性、惯用性等?
我已经尝试或考虑过的
天真的包装 asclass_<Node>(m, "Node")
不起作用:Python 实例无法知道节点是否仍然有效。基本上,返回Node*
viatake_ownership
会导致双重删除,返回它们 viareference
或reference_internal
会导致未定义的行为(读取、分段错误)。
所以我们要么需要一些PyNode
蹦床类通过其他方式跟踪节点的有效性(例如,注册回调,如Node::onAboutToDie()
),要么更改 C++ 所有权模型以使用引用计数的智能指针。
我认为一个不错的选择是将 C++ 代码更改为使用shared_ptr
而不是unique_ptr
. 我会Node
派生自enable_shared_from_this
,并将其包装为class_<Node, PyWeakPtr<Node>>(m, "Node")
在引擎盖下PyWeakPtr
存储 a 的自定义持有人的位置。weak_ptr
由于节点派生自enable_shared_from_this
,因此持有者可以有一个PyWeakPtr(T*)
构造函数将原始指针转换为weak_ptr
. 简而言之,这将利用共享指针的引用计数能力不具有共享所有权(所有权仍然是唯一的),而只是为了能够在 Python 端使用弱引用,这将检查expired()
在每次访问之前,如果过期则抛出一个可捕获的异常。然而,到目前为止,我的各种尝试都失败了:要么我无法获得预期的行为,要么我什至无法让它们编译,等等。
一个更明显的解决方案也是shared_ptr
使用class_<Node, std::shared_ptr<Node>>(m, "Node")
而不是我们的自定义 holder PyWeakPtr
,但它也不起作用,因为只要一些 python 变量指向节点,它就会人为地延长节点的生命周期。我认为这是一个泄漏:节点不应该被共享,如果用户在 UI 中删除一个节点,它应该消失并且立即调用析构函数。语义确实应该是在 Python 中访问“语义删除”节点会引发 Python 错误(不会使 C++ 程序崩溃),而不是默默地伪装成有效节点。
如果有用,我可能会花时间清理/make_minimal/etc 中的一些尝试,但是为了简洁起见,现在我将保留问题,也许这里的一些专家已经有了一些有用的见解,例如整个方法是否甚至是有道理的,等等 :)
请注意,许多解决类似问题的现有软件(例如 QtQDomDocument
或 Pixar 的通用场景描述)即使在 C++ API 中也倾向于使用引用计数的弱引用(或多或少在内部隐藏),从而完全屏蔽了 C++ 和 Python 客户端等错误。我也对这种方法持开放态度,尽管理想情况下,我认为我更愿意坚持在 C++ 中简单地使用非拥有原始指针的一般准则。