对象所有权#
对于绑定开发者来说,主要考虑的一点是 C++ 实例的生命周期如何与 Python 的引用计数相匹配。你最不希望的就是 C++ 实例被删除后,包装对象尝试访问无效内存,导致程序崩溃。
在本节中,我们将展示 Qt for Python 是如何利用 APIExtractor 提供的信息来处理对象所有权和父子关系的。
所有权基础知识#
作为任何 Python 绑定,Qt for Python 基础的绑定使用引用计数来处理包装对象(包含 C++ 对象的 Python 对象,不要与 包装的 C++ 对象混淆)。当引用计数达到零时,包装对象将被 Python 垃圾回收器删除,并尝试删除包装的实例,但有时包装的 C++ 对象已被删除,或者也许在 Python 包装对象超出作用域并死亡后,不应该释放 C++ 对象,因为 C++ 已经在处理包装的实例。
这并不是指由 值类型 指定的值类型,这些类型可以自由创建、复制和销毁,但是由 对象类型 指定的指向具有生命周期的 C++ 实例,可能需要关注。
为了处理这个问题,你应该告诉生成器实例的所有权属于绑定还是属于 C++ 库。如果是属于绑定,我们就可以确信 C++ 对象不会被 C++ 代码删除,并且在引用计数达到 0 时可以调用 C++ 析构函数。否则,由 C++ 代码拥有的实例可以随意被销毁,而不会通知 Python 包装器其销毁。
默认情况下,在 Python 中创建的对象具有所有权。相关的情况包括在 Python 中实现的(C++ 包装代码)虚拟工厂方法返回的值,这些方法将绑定代码传递出去。从 C++ 获得的对象(例如,QGuiApplication::clipboard()
)没有所有权。
Shiboken 模块 提供了 dump()
工具函数,该函数会打印对象的相关信息。
使对象无效#
为了防止段错误和双重释放,包装对象会被使无效。一个无效的对象不能作为参数传递或访问属性或方法。尝试这样做将引发 RuntimeError。
以下情况可以使对象无效
C++ 获取所有权#
当一个对象被传递给一个会获取其所有权的方法或函数时,包装器会失效,因为我们无法确定对象何时被销毁,除非它有一个虚析构函数或转移是由于《父级所有权》的特殊情况。
除了作为参数传递之外,被调用的对象其所有权也可能发生变化,就像Qt的《QObject》中的《setParent》方法。
使用后失效#
类型系统描述中带有《invalidate-after-use》标记的对象始终是来自C++调用的提供虚方法的对象。它们应该在Python函数返回后立即失效(见《使用后失效》)。
具有虚方法的对象#
一些实现细节(另请参阅《代码生成术语》):通过创建一个从具有虚方法的类继承的C++类(即《shell》),并覆盖这些方法来检查Python中是否有继承类覆盖了这些方法来支持虚方法。
如果类具有虚析构函数(C++类应该具有虚方法),则此C++实例仅在调用重写的析构函数时使包装器失效。
在Python中创建时创建了一个《shell》的实例。然而,在C++中创建对象,如工厂方法或虚拟函数(如《QObject::event(QEvent*)
)的参数时,包装的对象是原生类的C++实例,而不是《shell》实例,我们无法知道它何时被销毁。
父子关系#
一种特殊的所有权类型是父子关系。成为对象的子级意味着当对象的父级死亡时,C++实例也会死亡,因此Python引用将会失效。例如,Qt的QObject系统实现了这种行为,但这是对具有类似行为的任何C++库都有效的。
父子关系启发式算法#
正如父子关系非常常见,Qt for Python试图自动推断哪些方法属于父子关系方案,并添加与所有权相关的额外指令。
此启发式算法将在为方法生成代码时触发
函数是一个构造函数。
参数名称是《parent》。
参数类型是对对象的指针。
触发时,启发式算法将名为“parent”的参数设置为构造函数创建的对象的父级。
此过程的主要目的是从类型系统中去除大量手写代码,以便绑定Qt库。对于Qt,此启发式算法在所有情况下都有效,但请注意,绑定您自己的库时可能不适用。
要激活此启发式算法,请使用–enable-parent-ctor-heuristic命令行开关。
返回值启发式算法#
当启用时,C++中以指针返回的对象将成为调用方法的对象的子级。
要激活此启发式算法,请使用–enable-return-value-heuristic命令行开关。
要为特定情况禁用此启发式算法,请指定default
作为所有权
<modify-argument index="0">
<define-ownership class="target" owner="default" />
</modify-argument>
常见错误#
未保存非所有对象引用#
有时候当你将一个实例作为参数传递给一个方法,并且被接收的实例需要该对象无限期地存在,但不会接管该参数实例时,你应该保留参数实例的引用。
例如,假设你有一个渲染器类,它将在 setSource 方法中使用源类,但不会接管它。下面的代码是错误的,因为当调用 render 时,在调用 setSource 时创建的 Source 对象已被销毁。
renderer.setModel(Source())
renderer.render()
为了解决这个问题,你应该保留源对象的引用,如下所示:
source = Source()
renderer.setSource(source)
renderer.render()
类型系统中的所有权管理#
Python 封装代码#
对于此代码,class
属性取值为 target
(参见 代码生成术语)。
C++ 到目标的所有权转移#
当一个由 C++ 管理的所有权对象的所有权转移到目标语言后,绑定可以确定对象何时被删除,并将 C++ 实例的存在与封装器绑定,在封装器被删除时,正常调用 C++ 析构函数。
<modify-argument index="1">
<define-ownership class="target" owner="target" />
</modify-argument>
一个典型的用例是从 C++ 分配的对象返回,例如从 clone()
或其他工厂方法中返回。
从目标到 C++ 的所有权转移#
在相反的方向上,当一个对象的所有权从目标语言转移到 C++ 时,原生代码将完全控制对象的生命周期,你不知道该对象何时会被删除,从而使得封装器对象无效,除非你封装了一个具有虚析构函数的对象,这样你就可以重写它并在其销毁时得到通知。
默认情况下,当用户尝试访问这些对象的成员或将其作为参数传递给某个函数时,最好将封装器对象设置为无效并引发一些错误,以避免不愉快的段错误。你还应避免在删除封装器时调用 C++ 析构函数。
<modify-argument index="1">
<define-ownership class="target" owner="c++" />
</modify-argument>
用例包括通过指针返回成员对象或将对象通过指针传递到函数中,其中类接管所有权,例如 QNetworkAccessManager::setCookieJar(QNetworkCookieJar *)
。
父子关系#
有一种特殊的关系是父子关系。当一个对象被称为另一个对象(子对象)的父对象时,前一个对象在删除时负责删除其子对象,目标语言可以信赖,只要父对象存在,子对象就会存在,除非有其他方法可以将 C++ 的所有权从中夺走。
该方案的主要用途之一是 Qt 的对象系统,在 QObject 派生类之间进行所有权布局,创建“实例树”。
<modify-argument index="this">
<parent index="1" action="add"/>
</modify-argument>
在这个例子中,带有正在调用方法(在 modify-argument 上由 'index="this"' 指示)的实例将被标记为第一个参数的子代,使用 parent 标签。要取消所有权,只需使用动作属性中的“remove”。取消父子关系也会将所有权转让回 Python。
参见 Qt 中的对象树和对象所有权。
C++ 封装代码#
对于此代码,class
属性的值取为 native
。修改将对从 C++ 中调用的代码产生影响,通常是在调用在 Python 中重新实现的虚拟 C++ 方法时(参见代码生成术语)。
虚拟函数的返回值#
返回指针的 C++ 对象的所有权应设置为 c++
,以防止 Python 销毁它们,因为 Python 中创建的对象默认具有所有权。
为其他参数指定的所有权转移没有任何效果。
使用后失效#
有时一个对象在 C++ 中被创建并作为虚拟方法调用的参数传递,并在调用返回后销毁(参见具有虚拟方法的对象)。在这种情况下,您应该在modify-argument 标签中使用 invalidate-after-use
属性来标记封装在虚拟方法返回后立即无效。
<modify-argument index="2" invalidate-after-use="yes"/>
在此示例中,第二个参数将在此方法调用后失效。