我正在研究 pybind11 中的一个测试文件,并遇到了keep_alive.
py::keep_alive<1, 2>
py::keep_alive<1, 0>
py::keep_alive<0, 1>
Run Code Online (Sandbox Code Playgroud)
有人可以阐明这个测试文件中的这些用法吗?我知道索引指0的是返回,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>());
Run Code Online (Sandbox Code Playgroud)
在实际代码中,该addChild函数将通过将对象存储到指针来实现Parent,而不获取所有权(即以后不会在 C++ 端删除它)。作用py::keep_alive<1, 2>是将Parent对象的引用放到传递Child给 的对象上addChild,从而将 的生命周期Child与 的生命周期联系起来Parent。
所以,如果写:
p = Parent()
p.addChild(Child())
Run Code Online (Sandbox Code Playgroud)
那么如果没有keep_alive,该临时Child对象将在下一行超出范围(引用计数减至零)。相反,使用 时keep_alive<1, 2>,会发生以下情况(伪代码):
p = Parent()
c = Child()
p.__keep_alive = c
p.addChild(c)
del c
Run Code Online (Sandbox Code Playgroud)
因此,现在当p超出范围时,其数据会被清理,包括。参考__keep_alive,此时c也会被清理。意思是,p“临时”子项c同时超出范围而不是更早。
编辑:对于keep_alive<0, 1>,隐式的生命周期this与返回值相关。在测试中,它仅用于验证该策略是否可以与 None 返回一起使用,但在访问临时的内部数据项时很常见,通常会处理长语句上的中间临时数据,如下所示:
c = getCopyOfData().at(0).getField('f')
Run Code Online (Sandbox Code Playgroud)
问题在于,在 C++ 中,临时变量的生命周期直到语句结束为止,因此上述情况在音译代码中很常见。getCopyOfData()但在Python中,它以引用计数变为0而结束。也就是说,调用结束后结果将消失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
Run Code Online (Sandbox Code Playgroud)
因此,现在复制的数据容器d不会超出范围,直到对访问字段的引用超出范围。
对于 来说keep_alive<1, 0>,返回值的生命周期与隐式的有关this。如果所有权传递给调用者,而隐式this保留指针,则这非常有用,实际上将内存管理从 C++ 推迟到 Python。请记住,在 pybind11 中,对象标识被保留,因此任何returnChildKeepAlive返回相同指针的调用都将产生相同的 Python 对象,而不是新对象。所以在这种情况下(伪代码):
c = p.returnChildKeepAlive() # with c now owning the C++ object
p.__keep_alive = c
Run Code Online (Sandbox Code Playgroud)
如果引用c首先超出范围,p仍将使其保持活动状态,以免被悬空指针卡住。如果p先超出范围,c不会受到影响,因为它接管了所有权(即,C++ 端不会被删除)。如果returnChildKeepAlive()第二次调用,它将返回对突出的引用c,而不是新的代理,因此不会影响整体生命周期管理。