pybind11 保持对象存活

pybind11 keeping objects alive

我正在研究 pybind11 中的一个测试文件,发现 keep_alive.

的不同用法
py::keep_alive<1, 2>
py::keep_alive<1, 0>
py::keep_alive<0, 1>

有人可以阐明此测试文件中的这些用法吗?我知道索引 0 指的是 return,1 指向 this 指针。我只能理解 py::keep_alive<1, 2>(使用文档),但不能理解它在这个测试文件中的用法。

class Child {
public:
    Child() { py::print("Allocating child."); }
    Child(const Child &) = default;
    Child(Child &&) = default;
    ~Child() { py::print("Releasing child."); }
};
py::class_<Child>(m, "Child")
    .def(py::init<>());

class Parent {
public:
    Parent() { py::print("Allocating parent."); }
    ~Parent() { py::print("Releasing parent."); }
    void addChild(Child *) { }
    Child *returnChild() { return new Child(); }
    Child *returnNullChild() { return nullptr; }
};
py::class_<Parent>(m, "Parent")
    .def(py::init<>())
    .def(py::init([](Child *) { return new Parent(); }), py::keep_alive<1, 2>())
    .def("addChild", &Parent::addChild)
    .def("addChildKeepAlive", &Parent::addChild, py::keep_alive<1, 2>())
    .def("returnChild", &Parent::returnChild)
    .def("returnChildKeepAlive", &Parent::returnChild, py::keep_alive<1, 0>())
    .def("returnNullChildKeepAliveChild", &Parent::returnNullChild, py::keep_alive<1, 0>())
    .def("returnNullChildKeepAliveParent", &Parent::returnNullChild, py::keep_alive<0, 1>());

在实际代码中,addChild 函数将通过将 Parent 对象存储到指针来实现,而不获取所有权(即它不会稍后在 C++ 端删除它)。 py::keep_alive<1, 2> 所做的是将 Parent 对象的引用放到传递给 addChildChild 对象上,从而将 Child 的生命周期与 Child 的生命周期联系起来Parent.

所以,如果写:

p = Parent()
p.addChild(Child())

如果没有 keep_alive,那个临时 Child 对象将在下一行超出范围(引用计数减为零)。相反,使用 keep_alive<1, 2>,会发生以下情况(伪代码):

p = Parent()
c = Child()
p.__keep_alive = c
p.addChild(c)
del c

所以现在当 p 超出范围时,它的数据会被清理,包括。 __keep_alive 引用,此时 c 也被清理。意思是,p 和 "temporary" 子 c 同时超出范围,而不是更早。

编辑:对于keep_alive<0, 1>,隐式this 的生命周期与return 值相关联。在测试中,它仅用于验证该策略是否可以与 None return 一起使用,但在访问临时文件的内部数据项时很常见,通常在长语句中处理中间临时文件,像这样:

c = getCopyOfData().at(0).getField('f')

问题在于,在 C++ 中,临时对象的生命周期一直持续到语句结束,因此上述内容在音译代码中很常见。但在 Python 中,它以引用计数变为 0 结束。现在,getCopyOfData() 的结果将在调用 at(0) 完成后消失,留下 getField() 点被删除记忆。相反,使用 keep_alive<0, 1>,它将是(伪代码):

d = getCopyOfData()
at0 = d.at(0)
at0.__keep_alive = d
del d
c = at0.getField('f')
c.__keep_alive = at0
del at0

所以现在复制的数据容器 d 不会超出范围,直到对访问字段的引用超出范围。

对于 keep_alive<1, 0>,return 值的生命周期与隐式 this 相关联。如果所有权被传递给调用者,这很有用,而隐式 this 保留一个指针,实际上将内存管理从 C++ 推迟到 Python。请记住,在 pybind11 中,对象标识被保留,因此对 returnChildKeepAlive return 相同指针的任何调用都将导致相同的 Python 对象,而不是新对象。所以在这种情况下(伪代码):

c = p.returnChildKeepAlive()    # with c now owning the C++ object
p.__keep_alive = c

如果引用 c 首先超出范围,p 仍会保持它的活动状态,以免被悬挂指针卡住。如果 p 先超出范围,c 不会受到影响,因为它接管了所有权(也就是说,C++ 端不会被删除)。如果 returnChildKeepAlive() 被第二次调用,它将 return 引用未完成的 c,而不是新代理,因此不会影响整体生命周期管理。