对象所有权#

对于绑定开发者来说,主要考虑的一点是 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"/>

在此示例中,第二个参数将在此方法调用后失效。