Add static_asserts to enforce that py::smart_holder is combined with py::trampoline_self_life_support (#5633)

* Strictly enforce: trampoline must inherit from trampoline_self_life_support when used in combination with smart_holder

* Simplify test_class_sh_trampoline_basic.cpp,py (only one Abase is needed now)

* Replace obsolete sophisticated `throw value_error()` with a simple `assert()`

* Strictly enforce: trampoline should inherit from trampoline_self_life_support only if used in combination with smart_holder

* Resolve clang-tidy error

```
/__w/pybind11/pybind11/tests/test_class_sh_trampoline_basic.cpp:35:46: error: the parameter 'obj' is copied for each invocation but only used as a const reference; consider making it a const reference [performance-unnecessary-value-param,-warnings-as-errors]
   35 | int AddInCppSharedPtr(std::shared_ptr<Abase> obj, int other_val) {
      |                                              ^
      |                       const                 &
```

* Disable new static_assert if PYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE is defined.
This commit is contained in:
Ralf W. Grosse-Kunstleve
2025-04-30 22:12:13 -07:00
committed by GitHub
parent c7f3460f18
commit c630e22c1c
7 changed files with 56 additions and 87 deletions

View File

@@ -82,15 +82,21 @@ helper class that is defined as follows:
The ``py::trampoline_self_life_support`` base class is needed to ensure The ``py::trampoline_self_life_support`` base class is needed to ensure
that a ``std::unique_ptr`` can safely be passed between Python and C++. To that a ``std::unique_ptr`` can safely be passed between Python and C++. To
steer clear of notorious pitfalls (e.g. inheritance slicing), it is best help you steer clear of notorious pitfalls (e.g. inheritance slicing),
practice to always use the base class, in combination with pybind11 enforces that trampoline classes inherit from
``py::trampoline_self_life_support`` if used in in combination with
``py::smart_holder``. ``py::smart_holder``.
.. note:: .. note::
For completeness, the base class has no effect if a holder other than For completeness, the base class has no effect if a holder other than
``py::smart_holder`` used, including the default ``std::unique_ptr<T>``. ``py::smart_holder`` used, including the default ``std::unique_ptr<T>``.
Please think twice, though, the pitfalls are very real, and the overhead To avoid confusion, pybind11 will fail to compile bindings that combine
for using the safer ``py::smart_holder`` is very likely to be in the noise. ``py::trampoline_self_life_support`` with a holder other than
``py::smart_holder``.
Please think twice, though, before deciding to not use the safer
``py::smart_holder``. The pitfalls associated with avoiding it are very
real, and the overhead for using it is very likely in the noise.
The macro :c:macro:`PYBIND11_OVERRIDE_PURE` should be used for pure virtual The macro :c:macro:`PYBIND11_OVERRIDE_PURE` should be used for pure virtual
functions, and :c:macro:`PYBIND11_OVERRIDE` should be used for functions which have functions, and :c:macro:`PYBIND11_OVERRIDE` should be used for functions which have

View File

@@ -827,11 +827,8 @@ struct load_helper : value_and_holder_helper {
auto *self_life_support auto *self_life_support
= dynamic_raw_ptr_cast_if_possible<trampoline_self_life_support>(raw_type_ptr); = dynamic_raw_ptr_cast_if_possible<trampoline_self_life_support>(raw_type_ptr);
if (self_life_support == nullptr && python_instance_is_alias) { // This is enforced indirectly by a static_assert in the class_ implementation:
throw value_error("Alias class (also known as trampoline) does not inherit from " assert(!python_instance_is_alias || self_life_support);
"py::trampoline_self_life_support, therefore the ownership of this "
"instance cannot safely be transferred to C++.");
}
std::unique_ptr<D> extracted_deleter = holder().template extract_deleter<T, D>(context); std::unique_ptr<D> extracted_deleter = holder().template extract_deleter<T, D>(context);

View File

@@ -20,6 +20,7 @@
#include "gil.h" #include "gil.h"
#include "gil_safe_call_once.h" #include "gil_safe_call_once.h"
#include "options.h" #include "options.h"
#include "trampoline_self_life_support.h"
#include "typing.h" #include "typing.h"
#include <cassert> #include <cassert>
@@ -1982,7 +1983,21 @@ public:
"Unknown/invalid class_ template parameters provided"); "Unknown/invalid class_ template parameters provided");
static_assert(!has_alias || std::is_polymorphic<type>::value, static_assert(!has_alias || std::is_polymorphic<type>::value,
"Cannot use an alias class with a non-polymorphic type"); "Cannot use an alias class (aka trampoline) with a non-polymorphic type");
#ifndef PYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE
static_assert(!has_alias || !detail::is_smart_holder<holder_type>::value
|| std::is_base_of<trampoline_self_life_support, type_alias>::value,
"Alias class (aka trampoline) must inherit from"
" pybind11::trampoline_self_life_support if used in combination with"
" pybind11::smart_holder");
#endif
static_assert(!has_alias || detail::is_smart_holder<holder_type>::value
|| !std::is_base_of<trampoline_self_life_support, type_alias>::value,
"pybind11::trampoline_self_life_support is a smart_holder feature, therefore"
" an alias class (aka trampoline) should inherit from"
" pybind11::trampoline_self_life_support only if used in combination with"
" pybind11::smart_holder");
PYBIND11_OBJECT(class_, generic_type, PyType_Check) PYBIND11_OBJECT(class_, generic_type, PyType_Check)

View File

@@ -64,7 +64,7 @@ struct with_alias {
with_alias &operator=(const with_alias &) = default; with_alias &operator=(const with_alias &) = default;
with_alias &operator=(with_alias &&) = default; with_alias &operator=(with_alias &&) = default;
}; };
struct with_alias_alias : with_alias {}; struct with_alias_alias : with_alias, py::trampoline_self_life_support {};
struct sddwaa : std::default_delete<with_alias_alias> {}; struct sddwaa : std::default_delete<with_alias_alias> {};
} // namespace class_sh_factory_constructors } // namespace class_sh_factory_constructors

View File

@@ -5,7 +5,6 @@
namespace pybind11_tests { namespace pybind11_tests {
namespace class_sh_trampoline_basic { namespace class_sh_trampoline_basic {
template <int SerNo> // Using int as a trick to easily generate a series of types.
struct Abase { struct Abase {
int val = 0; int val = 0;
virtual ~Abase() = default; virtual ~Abase() = default;
@@ -20,63 +19,39 @@ struct Abase {
Abase &operator=(Abase &&) noexcept = default; Abase &operator=(Abase &&) noexcept = default;
}; };
template <int SerNo> struct AbaseAlias : Abase, py::trampoline_self_life_support {
struct AbaseAlias : Abase<SerNo> { using Abase::Abase;
using Abase<SerNo>::Abase;
int Add(int other_val) const override { int Add(int other_val) const override {
PYBIND11_OVERRIDE_PURE(int, /* Return type */ PYBIND11_OVERRIDE_PURE(int, /* Return type */
Abase<SerNo>, /* Parent class */ Abase, /* Parent class */
Add, /* Name of function in C++ (must match Python name) */ Add, /* Name of function in C++ (must match Python name) */
other_val); other_val);
} }
}; };
template <> int AddInCppRawPtr(const Abase *obj, int other_val) { return obj->Add(other_val) * 10 + 7; }
struct AbaseAlias<1> : Abase<1>, py::trampoline_self_life_support {
using Abase<1>::Abase;
int Add(int other_val) const override { int AddInCppSharedPtr(const std::shared_ptr<Abase> &obj, int other_val) {
PYBIND11_OVERRIDE_PURE(int, /* Return type */
Abase<1>, /* Parent class */
Add, /* Name of function in C++ (must match Python name) */
other_val);
}
};
template <int SerNo>
int AddInCppRawPtr(const Abase<SerNo> *obj, int other_val) {
return obj->Add(other_val) * 10 + 7;
}
template <int SerNo>
int AddInCppSharedPtr(std::shared_ptr<Abase<SerNo>> obj, int other_val) {
return obj->Add(other_val) * 100 + 11; return obj->Add(other_val) * 100 + 11;
} }
template <int SerNo> int AddInCppUniquePtr(std::unique_ptr<Abase> obj, int other_val) {
int AddInCppUniquePtr(std::unique_ptr<Abase<SerNo>> obj, int other_val) {
return obj->Add(other_val) * 100 + 13; return obj->Add(other_val) * 100 + 13;
} }
template <int SerNo>
void wrap(py::module_ m, const char *py_class_name) {
py::classh<Abase<SerNo>, AbaseAlias<SerNo>>(m, py_class_name)
.def(py::init<int>(), py::arg("val"))
.def("Get", &Abase<SerNo>::Get)
.def("Add", &Abase<SerNo>::Add, py::arg("other_val"));
m.def("AddInCppRawPtr", AddInCppRawPtr<SerNo>, py::arg("obj"), py::arg("other_val"));
m.def("AddInCppSharedPtr", AddInCppSharedPtr<SerNo>, py::arg("obj"), py::arg("other_val"));
m.def("AddInCppUniquePtr", AddInCppUniquePtr<SerNo>, py::arg("obj"), py::arg("other_val"));
}
} // namespace class_sh_trampoline_basic } // namespace class_sh_trampoline_basic
} // namespace pybind11_tests } // namespace pybind11_tests
using namespace pybind11_tests::class_sh_trampoline_basic; using namespace pybind11_tests::class_sh_trampoline_basic;
TEST_SUBMODULE(class_sh_trampoline_basic, m) { TEST_SUBMODULE(class_sh_trampoline_basic, m) {
wrap<0>(m, "Abase0"); py::classh<Abase, AbaseAlias>(m, "Abase")
wrap<1>(m, "Abase1"); .def(py::init<int>(), py::arg("val"))
.def("Get", &Abase::Get)
.def("Add", &Abase::Add, py::arg("other_val"));
m.def("AddInCppRawPtr", AddInCppRawPtr, py::arg("obj"), py::arg("other_val"));
m.def("AddInCppSharedPtr", AddInCppSharedPtr, py::arg("obj"), py::arg("other_val"));
m.def("AddInCppUniquePtr", AddInCppUniquePtr, py::arg("obj"), py::arg("other_val"));
} }

View File

@@ -1,11 +1,9 @@
from __future__ import annotations from __future__ import annotations
import pytest
from pybind11_tests import class_sh_trampoline_basic as m from pybind11_tests import class_sh_trampoline_basic as m
class PyDrvd0(m.Abase0): class PyDrvd(m.Abase):
def __init__(self, val): def __init__(self, val):
super().__init__(val) super().__init__(val)
@@ -13,47 +11,25 @@ class PyDrvd0(m.Abase0):
return self.Get() * 100 + other_val return self.Get() * 100 + other_val
class PyDrvd1(m.Abase1): def test_drvd_add():
def __init__(self, val): drvd = PyDrvd(74)
super().__init__(val)
def Add(self, other_val):
return self.Get() * 200 + other_val
def test_drvd0_add():
drvd = PyDrvd0(74)
assert drvd.Add(38) == (74 * 10 + 3) * 100 + 38 assert drvd.Add(38) == (74 * 10 + 3) * 100 + 38
def test_drvd0_add_in_cpp_raw_ptr(): def test_drvd_add_in_cpp_raw_ptr():
drvd = PyDrvd0(52) drvd = PyDrvd(52)
assert m.AddInCppRawPtr(drvd, 27) == ((52 * 10 + 3) * 100 + 27) * 10 + 7 assert m.AddInCppRawPtr(drvd, 27) == ((52 * 10 + 3) * 100 + 27) * 10 + 7
def test_drvd0_add_in_cpp_shared_ptr(): def test_drvd_add_in_cpp_shared_ptr():
while True: while True:
drvd = PyDrvd0(36) drvd = PyDrvd(36)
assert m.AddInCppSharedPtr(drvd, 56) == ((36 * 10 + 3) * 100 + 56) * 100 + 11 assert m.AddInCppSharedPtr(drvd, 56) == ((36 * 10 + 3) * 100 + 56) * 100 + 11
return # Comment out for manual leak checking (use `top` command). return # Comment out for manual leak checking (use `top` command).
def test_drvd0_add_in_cpp_unique_ptr(): def test_drvd_add_in_cpp_unique_ptr():
while True: while True:
drvd = PyDrvd0(0) drvd = PyDrvd(25)
with pytest.raises(ValueError) as exc_info: assert m.AddInCppUniquePtr(drvd, 83) == ((25 * 10 + 3) * 100 + 83) * 100 + 13
m.AddInCppUniquePtr(drvd, 0)
assert (
str(exc_info.value)
== "Alias class (also known as trampoline) does not inherit from"
" py::trampoline_self_life_support, therefore the ownership of this"
" instance cannot safely be transferred to C++."
)
return # Comment out for manual leak checking (use `top` command).
def test_drvd1_add_in_cpp_unique_ptr():
while True:
drvd = PyDrvd1(25)
assert m.AddInCppUniquePtr(drvd, 83) == ((25 * 10 + 3) * 200 + 83) * 100 + 13
return # Comment out for manual leak checking (use `top` command). return # Comment out for manual leak checking (use `top` command).

View File

@@ -28,7 +28,7 @@ struct SpBase {
std::shared_ptr<SpBase> pass_through_shd_ptr(const std::shared_ptr<SpBase> &obj) { return obj; } std::shared_ptr<SpBase> pass_through_shd_ptr(const std::shared_ptr<SpBase> &obj) { return obj; }
struct PySpBase : SpBase { struct PySpBase : SpBase, py::trampoline_self_life_support {
using SpBase::SpBase; using SpBase::SpBase;
bool is_base_used() override { PYBIND11_OVERRIDE(bool, SpBase, is_base_used); } bool is_base_used() override { PYBIND11_OVERRIDE(bool, SpBase, is_base_used); }
}; };