fix: avoid copy constructor instantiation in shared_ptr fallback cast (#6028)
* tests: add regressions for shared_ptr reference_internal fallback
* fix: avoid copy constructor instantiation in shared_ptr fallback cast
* Remove stray empty line
* tests: rename PyTorch shared_ptr regression test files
* refactor: add cast_non_owning helper for reference-like casts
Name the non-owning generic cast path so callers do not have to rediscover that
reference-like policies must pass null copy/move constructor callbacks. This
keeps the shared_ptr reference_internal fallback self-documenting and points
future maintainers toward the safe API.
Made-with: Cursor
* tests: guard deprecated-copy warning probes with __has_warning
Use __has_warning for the Clang-only regression test so older compiler jobs skip
unsupported warning groups instead of failing with -Wunknown-warning-option. A
simple __clang_major__ >= 13 guard would be shorter, but it bakes in a version
cutoff; __has_warning is slightly more verbose while being more robust to
vendor builds, backports, and future packaging differences.
Made-with: Cursor
---------
Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>
diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h
index bef0f2c..b7a4c2b 100644
--- a/include/pybind11/cast.h
+++ b/include/pybind11/cast.h
@@ -1027,7 +1027,7 @@
}
if (parent) {
- return type_caster_base<type>::cast(
+ return type_caster_generic::cast_non_owning(
srcs, return_value_policy::reference_internal, parent);
}
diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h
index b0c59e1..8fbf700 100644
--- a/include/pybind11/detail/type_caster_base.h
+++ b/include/pybind11/detail/type_caster_base.h
@@ -1004,6 +1004,18 @@
return cast(srcs, policy, parent, copy_constructor, move_constructor, existing_holder);
}
+ static handle cast_non_owning(const cast_sources &srcs,
+ return_value_policy policy,
+ handle parent,
+ const void *existing_holder = nullptr) {
+ // Reference-like policies alias an existing C++ object instead of creating
+ // a new one, so copy/move constructor callbacks must remain null here.
+ assert(policy == return_value_policy::reference
+ || policy == return_value_policy::reference_internal
+ || policy == return_value_policy::automatic_reference);
+ return cast(srcs, policy, parent, nullptr, nullptr, existing_holder);
+ }
+
PYBIND11_NOINLINE static handle cast(const cast_sources &srcs,
return_value_policy policy,
handle parent,
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index e87b1e9..d5d597c 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -167,6 +167,7 @@
test_operator_overloading
test_pickling
test_potentially_slicing_weak_ptr
+ test_pytorch_shared_ptr_cast_regression
test_python_multiple_inheritance
test_pytypes
test_scoped_critical_section
diff --git a/tests/test_class_sh_property.py b/tests/test_class_sh_property.py
index 4a7b77c..b8a5933 100644
--- a/tests/test_class_sh_property.py
+++ b/tests/test_class_sh_property.py
@@ -204,3 +204,15 @@
legacy = obj.legacy
legacy.value = 13
assert obj.legacy.value == 13
+
+
+def test_non_smart_holder_member_type_with_smart_holder_owner_aliases_member_multiple_reads():
+ obj = m.ShWithSimpleStructMember()
+
+ a = obj.legacy
+ b = obj.legacy
+
+ a.value = 13
+
+ assert b.value == 13
+ assert obj.legacy.value == 13
diff --git a/tests/test_pytorch_shared_ptr_cast_regression.cpp b/tests/test_pytorch_shared_ptr_cast_regression.cpp
new file mode 100644
index 0000000..1425962
--- /dev/null
+++ b/tests/test_pytorch_shared_ptr_cast_regression.cpp
@@ -0,0 +1,62 @@
+#include "pybind11_tests.h"
+
+#include <memory>
+#include <string>
+
+#if defined(__clang__)
+# if __has_warning("-Wdeprecated-copy-with-user-provided-dtor")
+# pragma clang diagnostic error "-Wdeprecated-copy-with-user-provided-dtor"
+# endif
+# if __has_warning("-Wdeprecated-copy-with-dtor")
+# pragma clang diagnostic error "-Wdeprecated-copy-with-dtor"
+# endif
+#endif
+
+namespace test_pytorch_regressions {
+
+// Directly extracted from PyTorch patterns that regressed in CI.
+struct TracingState : std::enable_shared_from_this<TracingState> {
+ TracingState() = default;
+ ~TracingState() = default;
+ int value = 0;
+};
+
+const std::shared_ptr<TracingState> &get_tracing_state() {
+ static std::shared_ptr<TracingState> state = std::make_shared<TracingState>();
+ return state;
+}
+
+struct InterfaceType {
+ ~InterfaceType() = default;
+ int value = 0;
+};
+using InterfaceTypePtr = std::shared_ptr<InterfaceType>;
+
+struct CompilationUnit {
+ InterfaceTypePtr iface = std::make_shared<InterfaceType>();
+
+ InterfaceTypePtr get_interface(const std::string &) const { return iface; }
+};
+
+} // namespace test_pytorch_regressions
+
+TEST_SUBMODULE(pybind11_pytorch_regressions, m) {
+ using namespace test_pytorch_regressions;
+
+ py::class_<TracingState, std::shared_ptr<TracingState>>(m, "TracingState")
+ .def(py::init<>())
+ .def_readwrite("value", &TracingState::value);
+
+ m.def("_get_tracing_state", []() { return get_tracing_state(); });
+
+ py::class_<InterfaceType, InterfaceTypePtr>(m, "InterfaceType")
+ .def(py::init<>())
+ .def_readwrite("value", &InterfaceType::value);
+
+ py::class_<CompilationUnit, std::shared_ptr<CompilationUnit>>(m, "CompilationUnit")
+ .def(py::init<>())
+ .def("get_interface",
+ [](const std::shared_ptr<CompilationUnit> &self, const std::string &name) {
+ return self->get_interface(name);
+ });
+}
diff --git a/tests/test_pytorch_shared_ptr_cast_regression.py b/tests/test_pytorch_shared_ptr_cast_regression.py
new file mode 100644
index 0000000..b7c393b
--- /dev/null
+++ b/tests/test_pytorch_shared_ptr_cast_regression.py
@@ -0,0 +1,25 @@
+from __future__ import annotations
+
+from pybind11_tests import pybind11_pytorch_regressions as m
+
+
+def test_pytorch_like_get_tracing_state_aliases_singleton_shared_ptr():
+ a = m._get_tracing_state()
+ b = m._get_tracing_state()
+
+ a.value = 17
+
+ assert b.value == 17
+ assert m._get_tracing_state().value == 17
+
+
+def test_pytorch_like_compilation_unit_get_interface_aliases_member_shared_ptr():
+ cu = m.CompilationUnit()
+
+ a = cu.get_interface("iface")
+ b = cu.get_interface("iface")
+
+ a.value = 23
+
+ assert b.value == 23
+ assert cu.get_interface("iface").value == 23