pybind11的返回值策略 return_value_policy
- 一、返回值策略的必要性
- 二、一个导致crash的例子
- 三、所有的返回值策略的探讨
- 四、补充说明
一、返回值策略的必要性
C++和python使用根本上就不一样的内存管理方法和对象生命周期管理方法。
C++是对栈变量按照作用域来析构,对堆变量使用手动delete来析构。Python则是使用基于计数的垃圾回收机制。当提供C++函数接口给python时,如果返回类型不是基本类型,这种内存管理的差异会产生一些问题。
只是看类型信息,看不出来Python是否应该接管返回值的所有权并最终回收它的资源,或是由C++端来处理。因为这个原因,pybind11提供了一些返回值策略标记传递给 module_::def()和class_::def()函数。
默认的策略是return_value_policy::automatic(自动管理返回值)。
二、一个导致crash的例子
返回值策略非常容易犯错,所以有必要真正弄懂它们。考虑下面一个例子:
struct Person{
int age;
int gender;
};
Person* one_person = new Person();
Person* get_one_person() { return one_person ; }
m.def("get_one_person", &get_one_person);
这里会发生什么?如果Python端调用了get_one_person(),那么返回类型被包装成一个python可用的类型。在这个场景,默认的返回值策略是return_value_policy::automatic(返回值自动管理),将会导致pybind11接管静态对象one_person的所有权。
当python的垃圾回收器最终删除python包装时,pybind11也会尝试析构这个C++对象(使用delete操作符)。这时整个应用程序最终会crash,尽管原因非常隐蔽,其实是静态对象内存损坏。
在上面这个例子中,应该使用return_value_policy::reference(引用返回值)策略。这种全局的静态对象one_person只是被python引用,而没有任何隐含的所有权转移。
m.def("get_one_person", &get_one_person, py::return_value_policy::reference);
三、所有的返回值策略的探讨
很多时候如果没有正确使用返回值策略,会导致内存泄露。作为使用pybind11的开发者,必须熟悉所有的返回值策略,清楚知道什么情况该用什么。下面是所有的返回值策略。
返回值策略 | C++返回值类型 | 描述 |
---|
return_value_policy::take_ownership(接管返回值所有权) | 指针 | 引用一个已经存在的对象然后接管它的所有权。Python会调用析构函数当这个对象的引用计数降至零时。警告:当C++端也去析构时,或者这个对象本身不是堆变量时,会发生未定义行为(极可能导致程序crash) |
return_value_policy::copy(复制返回值) | 值(左值) | 创建一个返回值的复制(会调用这个对象的复制构造函数),python端将使用这个复制对象。这个策略相对安全,因为两个对象的生命周期是分开的。 |
return_value_policy::move(移动返回值) | 值(右值) | 使用std::move来移动返回值到一个新对象中(会调用这个对象的移动构造函数),python端将使用这个新对象。这个策略相对安全,因为两个对象(移动来源和目标)的生命周期是分开的。对这个策略的理解必须要理解C++11引入的右值和移动语义概念。这个策略实质上也是接管了原对象所代理数据的所有权。 |
return_value_policy::reference(引用返回值) | 指针 | 引用一个已经存在的对象,并不接管它的所有权。C++端负责管理这个对象的生命周期和析构。警告:当C++端析构一个仍在被python使用的对象时,会发生未定义行为。 |
return_value_policy::reference_internal(内部引用返回值) | 指针 | 指示返回值的生命周期是绑定在父对象的生命周期的,即绑在这个属性或方法对应的this、self变量上。在内部实现上,这个策略类似return_value_policy::reference(引用返回值),但是额外增加了一个keep_alive<0, 1>调用策略,能够防止父对象被垃圾回收器回收只要这个返回值还在被python引用。这种策略是设置属性的方法getters 的默认策略,像def_property、def_readwrite。 |
return_value_policy::automatic(自动管理返回值) | 指针或值 | 当返回值是指针时,该策略就使用return_value_policy::take_ownership(接管返回值所有权)。当返回值是左值时,使用return_value_policy::copy(复制返回值)。当返回值是右值时,使用return_value_policy::move(移动返回值)。这个策略是py::class_包装类型的默认策略。 |
return_value_policy::automatic_reference(自动引用返回值) | 指针或值 | 区别于上面的自动管理返回值,这个策略对指针返回值使用return_value_policy::reference(引用返回值)策略,并不去接管它的所有权。这个是在python端调用那些C++生成的函数时,函数参数传递的默认策略。普通用户几乎很少用到。 |
返回值策略能应用在属性上:
class_<MyClass>(m, "MyClass")
.def_property("data", &MyClass::getData, &MyClass::setData,
py::return_value_policy::copy);
虽然上面的代码同时为getter和setter函数应用了返回值策略,但实际上setter并不关心返回值策略,只是一个语法糖。也有另一种写法,目标参数可以通过cpp_function 构造函数来传递。
class_<MyClass>(m, "MyClass")
.def_property("data"
py::cpp_function(&MyClass::getData, py::return_value_policy::copy),
py::cpp_function(&MyClass::setData)
);
四、补充说明
上述策略的一个重要方面是,它们只应用于pybind11以前没有见过的实例,在这种情况下,该策略理清了关于返回值的生命周期和所有权的基本问题。当pybind11已经知道实例(根据它在内存中的类型和地址标识)时,它将返回现有的Python对象包装器,而不是创建一个新的副本。即是针对新变量而不是已有的变量。
作为指定调用策略和生命周期管理逻辑的另一种选择,可以考虑使用智能指针。智能指针可以判断一个对象是否仍然被C++或Python引用,这通常会消除可能导致崩溃或未定义行为的不一致性。对于返回智能指针的函数,不需要指定返回值策略。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)