mirror of
https://github.com/pybind/pybind11.git
synced 2026-03-14 20:27:47 +00:00
[skip ci] Move "Pitfalls with raw pointers and shared ownership" section to a more prominent location. (#5611)
This commit is contained in:
committed by
GitHub
parent
1bd1d1ced8
commit
e38beb96c4
@@ -1,3 +1,5 @@
|
||||
.. _py_class_holder:
|
||||
|
||||
Smart pointers & ``py::class_``
|
||||
###############################
|
||||
|
||||
@@ -175,59 +177,3 @@ provides ``.get()`` functionality via ``.getPointer()``.
|
||||
The file :file:`tests/test_smart_ptr.cpp` contains a complete example
|
||||
that demonstrates how to work with custom reference-counting holder types
|
||||
in more detail.
|
||||
|
||||
|
||||
Be careful not to accidentally undermine automatic lifetime management
|
||||
======================================================================
|
||||
|
||||
``py::class_``-wrapped objects automatically manage the lifetime of the
|
||||
wrapped C++ object, in collaboration with the chosen holder type.
|
||||
When wrapping C++ functions involving raw pointers, care needs to be taken
|
||||
to not inadvertently transfer ownership, resulting in multiple Python
|
||||
objects acting as owners, causing heap-use-after-free or double-free errors.
|
||||
For example:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
class Child { };
|
||||
|
||||
class Parent {
|
||||
public:
|
||||
Parent() : child(std::make_shared<Child>()) { }
|
||||
Child *get_child() { return child.get(); } /* DANGER */
|
||||
private:
|
||||
std::shared_ptr<Child> child;
|
||||
};
|
||||
|
||||
PYBIND11_MODULE(example, m) {
|
||||
py::class_<Child, std::shared_ptr<Child>>(m, "Child");
|
||||
|
||||
py::class_<Parent, std::shared_ptr<Parent>>(m, "Parent")
|
||||
.def(py::init<>())
|
||||
.def("get_child", &Parent::get_child); /* PROBLEM */
|
||||
}
|
||||
|
||||
The following Python code leads to undefined behavior, likely resulting in
|
||||
a segmentation fault.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from example import Parent
|
||||
|
||||
print(Parent().get_child())
|
||||
|
||||
Part of the ``/* PROBLEM */`` here is that pybind11 falls back to using
|
||||
``return_value_policy::take_ownership`` as the default (see
|
||||
:ref:`return_value_policies`). The fact that the ``Child`` instance is
|
||||
already managed by ``std::shared_ptr<Child>`` is lost. Therefore pybind11
|
||||
will create a second independent ``std::shared_ptr<Child>`` that also
|
||||
claims ownership of the pointer, eventually leading to heap-use-after-free
|
||||
or double-free errors.
|
||||
|
||||
There are various ways to resolve this issue, either by changing
|
||||
the ``Child`` or ``Parent`` C++ implementations (e.g. using
|
||||
``std::enable_shared_from_this<Child>`` as a base class for
|
||||
``Child``, or adding a member function to ``Parent`` that returns
|
||||
``std::shared_ptr<Child>``), or if that is not feasible, by using
|
||||
``return_value_policy::reference_internal``. What is the best approach
|
||||
depends on the exact situation.
|
||||
|
||||
@@ -459,6 +459,64 @@ you can use ``py::detail::overload_cast_impl`` with an additional set of parenth
|
||||
other using the ``.def(py::init<...>())`` syntax. The existing machinery
|
||||
for specifying keyword and default arguments also works.
|
||||
|
||||
☝️ Pitfalls with raw pointers and shared ownership
|
||||
==================================================
|
||||
|
||||
``py::class_``-wrapped objects automatically manage the lifetime of the
|
||||
wrapped C++ object, in collaboration with the chosen holder type (see
|
||||
:ref:`py_class_holder`). When wrapping C++ functions involving raw pointers,
|
||||
care needs to be taken to not accidentally undermine automatic lifetime
|
||||
management. For example, ownership is inadvertently transferred here:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
class Child { };
|
||||
|
||||
class Parent {
|
||||
public:
|
||||
Parent() : child(std::make_shared<Child>()) { }
|
||||
Child *get_child() { return child.get(); } /* DANGER */
|
||||
private:
|
||||
std::shared_ptr<Child> child;
|
||||
};
|
||||
|
||||
PYBIND11_MODULE(example, m) {
|
||||
py::class_<Child, std::shared_ptr<Child>>(m, "Child");
|
||||
|
||||
py::class_<Parent, std::shared_ptr<Parent>>(m, "Parent")
|
||||
.def(py::init<>())
|
||||
.def("get_child", &Parent::get_child); /* PROBLEM */
|
||||
}
|
||||
|
||||
The following Python code leads to undefined behavior, likely resulting in
|
||||
a segmentation fault.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from example import Parent
|
||||
|
||||
print(Parent().get_child())
|
||||
|
||||
Part of the ``/* PROBLEM */`` here is that pybind11 falls back to using
|
||||
``return_value_policy::take_ownership`` as the default (see
|
||||
:ref:`return_value_policies`). The fact that the ``Child`` instance is
|
||||
already managed by ``std::shared_ptr<Child>`` is lost. Therefore pybind11
|
||||
will create a second independent ``std::shared_ptr<Child>`` that also
|
||||
claims ownership of the pointer, eventually leading to heap-use-after-free
|
||||
or double-free errors.
|
||||
|
||||
There are various ways to resolve this issue, either by changing
|
||||
the ``Child`` or ``Parent`` C++ implementations (e.g. using
|
||||
``std::enable_shared_from_this<Child>`` as a base class for
|
||||
``Child``, or adding a member function to ``Parent`` that returns
|
||||
``std::shared_ptr<Child>``), or if that is not feasible, by using
|
||||
``return_value_policy::reference_internal``. What is the best approach
|
||||
depends on the exact situation.
|
||||
|
||||
A highly effective way to stay in the clear — even in pure C++, but
|
||||
especially when binding C++ code to Python — is to consistently prefer
|
||||
``std::shared_ptr`` or ``std::unique_ptr`` over passing raw pointers.
|
||||
|
||||
.. _native_enum:
|
||||
|
||||
Enumerations and internal types
|
||||
|
||||
Reference in New Issue
Block a user