Object ownership¶
Python and C++ don’t manage the lifetime and storage of objects in the same way. Consequently, two questions arise whenever an object crosses the language barrier:
Who actually owns this object? C++? Python? Both?!
Can we safely determine when it is no longer needed?
This is important: we must exclude the possibility that Python destroys an object that is still being used by C++ (or vice versa).
The previous section introduced three ways of exchanging information between C++ and Python: type casters, bindings, and wrappers. It is specifically bindings for which these two questions must be answered.
A problematic example¶
Consider the following problematic example to see what can go wrong:
#include <nanobind/nanobind.h>
namespace nb = nanobind;
struct Data { };
Data data; // Data global variable & function returning a pointer to it
Data *get_data() { return &data; }
NB_MODULE(my_ext, m) {
nb::class_<Data>(m, "Data");
// KABOOM, calling this function will crash the Python interpreter
m.def("get_data", &get_data);
}
The bound function my_ext.get_data()
returns a Python object of type
my_ext.Data
that wraps the pointer &data
and takes ownership of it.
When Python eventually garbage collects the object, nanobind will try to free
the (non-heap-allocated) C++ instance via operator delete
, causing a
segmentation fault.
To avoid this problem, we can
Provide more information: the problem was that nanobind incorrectly transferred ownership of a C++ instance to the Python side. To fix this, we can add add a return value policy annotation that clarifies what to do with the return value.
Make ownership transfer explicit: C++ types passed via unique pointers (
std::unique_ptr<T>
) make the ownership transfer explicit in the type system, which would have revealed the problem in this example.Switch to shared ownership: C++ types passed via shared pointers (
std::shared_ptr<T>
), or which use intrusive reference counting can be shared by C++ and Python. The whole issue disappears because ownership transfer is no longer needed.
The remainder of this section goes through each of these options.
Return value policies¶
nanobind provides several return value policy annotations that can be
passed to module_::def()
, class_::def()
, and cpp_function()
.
The default policy is rv_policy::automatic
, which is usually
a reasonable default (but not in this case!).
In the problematic example, the policy
rv_policy::reference
should have been specified explicitly so
that the global instance is only referenced without any implied transfer of
ownership, i.e.:
m.def("get_data", &get_data, nb::rv_policy::reference);
On the other hand, this is not the right policy for many other situations, where ignoring ownership could lead to resource leaks. As a developer using this library, it is important that you familiarize yourself with the different options below. In particular, the following policies are available:
rv_policy::take_ownership
: Create a thin Python object wrapper around the returned C++ instance without making a copy and transfer ownership to Python. When the Python wrapper is eventually garbage collected, nanobind will call the C++delete
operator to free the C++ instance.In the example below, a function uses this policy to transfer ownership of a heap-allocated C++ instance to Python:
m.def("make_data", []{ return new Data(); }, nb::rv_policy::take_ownership);
The return value policy declaration could actually have been omitted here because
take_ownership
is the default for pointer return values (seeautomatic
).rv_policy::copy
: Copy-construct a new Python object from the C++ instance. The copy will be owned by Python, while C++ retains ownership of the original.In the example below, a function uses this policy to return a reference to a C++ instance. The owner and lifetime of such a reference may not be clear, so the safest route is to make a copy.
struct A { B &b() { /* .. unknown code .. */ } }; nb::class_<A>(m, "A") .def("b", &A::b, nb::rv_policy::copy);
The return value policy declaration could actually have been omitted here because
copy
is the default for lvalue reference return values (seeautomatic
).rv_policy::move
: Move-construct a new Python object from the C++ instance. The new object will be owned by Python, while C++ retains ownership of the original (whose contents were likely invalidated by the move operation).In the example below, a function uses this policy to return a C++ instance by value. The
copy
operation mentioned above would also be safe to use, but move construction has the potential of being significantly more efficient.struct A { B b() { return B(...); } }; nb::class_<A>(m, "A") .def("b", &A::b, nb::rv_policy::move);
The return value policy declaration could actually have been omitted here because
move
is the default for functions that return by value (seeautomatic
).rv_policy::reference
: Create a thin Python object wrapper around the returned C++ instance without making a copy, but do not transfer ownership to Python. nanobind will never call the C++delete
operator, even when the wrapper expires. The C++ side is responsible for destructing the C++ instance.This return value policy is dangerous and should be used cautiously. Undefined behavior will ensue when the C++ side deletes the instance while it is still being used by Python. If you need to use this policy, combine it with a
keep_alive
function binding annotation to manage the lifetime. Or use the simple and safereference_internal
alternative described next.Below is an example use of this return value policy to reference a global variable that does not need ownership and lifetime management.
Data data; // This is a global variable m.def("get_data", []{ return &data; }, nb::rv_policy::reference)
rv_policy::reference_internal
: A policy for methods that expose an internal field. The lifetime of the field must match that of the parent object.The policy resembles
reference
in that it creates creates a thin Python object wrapper around the returned C++ field without making a copy, and without transferring ownership to Python.Furthermore, it ensures that the instance owning the field (implicit
this
/self
argument) cannot be garbage collected while an object representing the field is alive.The example below uses this policy to implement a getter that permits mutable access to an internal field.
struct MyClass { public: MyField &field() { return m_field; } private: MyField m_field; }; nb::class_<MyClass>(m, "MyClass") .def("field", &MyClass::field, nb::rv_policy::reference_internal);
More advanced variations of this scheme are also possible using combinations of
reference
and thekeep_alive
function binding annotation.rv_policy::none
: This is the most conservative policy: it simply refuses the cast unless the C++ instance already has a corresponding Python object, in which case the question of ownership becomes moot.rv_policy::automatic
: This is the default return value policy, which falls back totake_ownership
when the return value is a pointer,move
when it is a rvalue reference, andcopy
when it is a lvalue reference.rv_policy::automatic_reference
: This policy matchesautomatic
but falls back toreference
when the return value is a pointer. It is the default for function arguments when calling Python functions from C++ code viadetail::api::operator()()
. You probably won’t need to use this policy in your own code.
Unique pointers¶
Passing a STL unique pointer embodies an ownership transfer—a return value
policy annotation is therefore not needed. To bind functions that receive or
return std::unique_ptr<..>
, add the extra include directive
#include <nanobind/stl/unique_ptr.h>
Note
While this this header file technically contains a type caster, it is not affected by their usual limitations (mandatory copy/conversion, inability to mutate function arguments).
Example: The following example binds two functions that create and consume
instances of a C++ type Data
via unique pointers.
#include <nanobind/stl/unique_ptr.h>
namespace nb = nanobind;
NB_MODULE(my_ext, m) {
struct Data { };
nb::class_<Data>(m, "Data");
m.def("create", []() { return std::make_unique<Data>(); });
m.def("consume", [](std::unique_ptr<Data> x) { /* no-op */ });
}
Calling a function taking a unique pointer from Python invalidates the passed Python object. nanobind will refuse further use of it:
Python 3.11.1 (main, Dec 23 2022, 09:28:24) [Clang 14.0.0 (clang-1400.0.29.202)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import my_ext
>>> x = my_ext.create()
>>> my_ext.consume(x)
>>> my_ext.consume(x)
<stdin>:1: RuntimeWarning: nanobind: attempted to access an uninitialized instance of type 'my_ext.Data'!
TypeError: consume(): incompatible function arguments. The following argument types are supported:
1. consume(arg: my_ext.Data, /) -> None
Invoked with types: my_ext.Data
We strongly recommend that you replace all use of std::unique_ptr<T>
by
std::unique_ptr<T, nb::deleter<T>>
in your code. Without the latter type
declaration, which references a custom nanobind-provided deleter
nb::deleter<T>
, nanobind cannot transfer ownership of
objects constructed using nb::init<...>
to C++ and will
refuse to do so with an error message. Further detail on this special case can
be found in the advanced section on object ownership.