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.