对象所有权¶
绑定开发者在考虑的主要问题之一是C++实例的生命周期如何与Python的引用计数相协调。你最不希望看到的是由于C++实例被删除,而包装对象试图访问无效内存,导致程序因段错误而崩溃。
在本节中,我们将展示Qt for Python如何处理对象所有权和父子关系,利用APIExtractor提供的信息。
所有权基础¶
与任何Python绑定一样,基于Python的Qt绑定使用引用计数来处理包装对象(包含C++对象的Python对象,不要与包装的C++对象混淆)的生命周期。当引用计数达到零时,包装对象会被Python垃圾回收器删除,并尝试删除包装的实例,但有时包装的C++对象已经被删除,或者C++对象在Python包装对象超出范围并死亡后不应被释放,因为C++已经在处理包装的实例。
这对于由value-type指定的值类型来说不是问题,它们可以自由创建、复制和销毁,然而由object-type指定的指向具有生命周期约束的C++实例的对象类型可能需要特别注意。
为了处理这个问题,你应该告诉生成器实例的所有权是属于绑定还是属于C++库。当所有权属于绑定时,我们可以确定C++对象不会被C++代码删除,并且当引用计数达到0时,我们可以调用C++析构函数。否则,由C++代码拥有的实例可以任意销毁,而不会通知Python包装器其销毁。
默认情况下,在 Python 中创建的对象具有所有权。一个相关的案例是在 Python 中重新实现的虚拟工厂方法的返回值(C++ 包装代码),这些方法会传递绑定代码。从 C++ 获取的对象(例如,QGuiApplication::clipoard())没有所有权。
Shiboken模块 提供了 dump() 实用函数,该函数打印对象的相关信息。
使对象无效¶
为了防止段错误和双重释放,包装器对象被置为无效。 一个无效的对象不能作为参数传递,也不能访问其属性或方法。 尝试这样做将会引发 RuntimeError。
以下情况可以使对象无效:
C++ 获取所有权¶
当一个对象被传递给一个函数或方法,该函数或方法拥有该对象的所有权时,包装器将失效,因为我们无法确定对象何时被销毁,除非它有一个虚析构函数或者转移是由于父所有权的特殊情况。
除了作为参数传递外,被调用的对象的所有权也可以被改变,例如Qt中的setParent方法在QObject中。
使用后失效¶
在类型系统描述中标记为使用后失效的对象始终是由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
(参见 Code Generation Terminology)。
所有权从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标签标记为第一个参数的子项。要移除所有权,只需在action属性中使用“remove”。移除父子关系也会将所有权转移回python。
C++ 封装代码¶
对于这段代码,class 属性取值为 native。这些修改会影响从C++内部调用的代码,通常是在调用在Python中重新实现的虚拟C++方法时(参见代码生成术语)。
虚函数的返回值¶
通过指针返回的C++对象的所有权应设置为c++,以防止它们被Python删除,因为在Python中创建的对象默认具有所有权。
为其他参数指定的所有权转移没有任何效果。
使用后失效¶
有时在C++中创建一个对象并将其作为虚方法调用的参数传递,并在调用返回后销毁
(参见具有虚方法的对象)。
在这种情况下,您应该在modify-argument标签中使用invalidate-after-use属性,以便在虚方法返回后立即将包装器标记为无效。
<modify-argument index="2" invalidate-after-use="yes"/>
在这个例子中,第二个参数将在该方法调用后失效。