From 89ba58996b42ecb663aecd6a639ee31a738f06b7 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 12 Aug 2026 01:59:42 -0400 Subject: [PATCH 1/2] fix: returning a copyable type with a deleted move constructor (#6142) detail::function_ref's constructor SFINAE required is_convertible, which is false for a copyable type whose move constructor is explicitly deleted (the trait tests conversion from an xvalue). In C++17, such a prvalue return is legal via guaranteed copy elision, so also accept an exact type match. Regression introduced in 3.1.0 by the call_impl outlining (#5887). Fixes #6142 Assisted-by: ClaudeCode:claude-fable-5 --- include/pybind11/detail/function_ref.h | 17 +++++++++++++++-- tests/test_copy_move.cpp | 23 +++++++++++++++++++++++ tests/test_copy_move.py | 11 +++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/include/pybind11/detail/function_ref.h b/include/pybind11/detail/function_ref.h index a81bdfe13f..c70e4ae15f 100644 --- a/include/pybind11/detail/function_ref.h +++ b/include/pybind11/detail/function_ref.h @@ -31,6 +31,8 @@ // - renamed back to function_ref // - use pybind11 enable_if_t, remove_cvref_t, and remove_reference_t // - lint suppressions +// - accept same-type non-movable returns under C++17 guaranteed copy elision +// (issue #6142) // torch::executor: modified from llvm::function_ref // - renamed to FunctionRef @@ -55,6 +57,17 @@ PYBIND11_NAMESPACE_BEGIN(detail) template class function_ref; +// pybind11: is_convertible is false for a copyable but non-movable +// type (it tests conversion from an xvalue, which selects the deleted move +// constructor), but in C++17 a same-type prvalue is returnable via guaranteed +// copy elision. Accept that case explicitly. See issue #6142. +template +using is_returnable_as = bool_constant< +#if defined(PYBIND11_CPP17) + std::is_same::value || +#endif + std::is_convertible::value>; + template class function_ref { Ret (*callback)(intptr_t callable, Params... params) = nullptr; @@ -81,8 +94,8 @@ class function_ref { // Functor must be callable and return a suitable type. enable_if_t< std::is_void::value - || std::is_convertible()(std::declval()...)), - Ret>::value> * = nullptr) + || is_returnable_as()(std::declval()...)), + Ret>::value> * = nullptr) : callback(callback_fn>), callable(reinterpret_cast(&callable)) {} diff --git a/tests/test_copy_move.cpp b/tests/test_copy_move.cpp index b163406a57..cd1c27215c 100644 --- a/tests/test_copy_move.cpp +++ b/tests/test_copy_move.cpp @@ -107,6 +107,22 @@ class CopyOnlyInt { int value; }; +// #6142: returning a copy-constructible type with an explicitly deleted move +// constructor failed to compile inside detail::function_ref (3.1.0 regression). +// Returning such a type by value requires C++17 guaranteed copy elision. +#if defined(PYBIND11_CPP17) +class CopyOnlyDeletedMove { +public: + explicit CopyOnlyDeletedMove(int v) : value{v} {} + CopyOnlyDeletedMove(const CopyOnlyDeletedMove &) = default; + CopyOnlyDeletedMove &operator=(const CopyOnlyDeletedMove &) = delete; + CopyOnlyDeletedMove(CopyOnlyDeletedMove &&) = delete; + CopyOnlyDeletedMove &operator=(CopyOnlyDeletedMove &&) = delete; + + int value; +}; +#endif + PYBIND11_NAMESPACE_BEGIN(pybind11) PYBIND11_NAMESPACE_BEGIN(detail) template <> @@ -176,6 +192,13 @@ TEST_SUBMODULE(copy_move_policies, m) { py::class_(m, "lacking_move_ctor") .def_static("get_one", &lacking_move_ctor::get_one, py::return_value_policy::move); + // test_copy_only_deleted_move (#6142) +#if defined(PYBIND11_CPP17) + py::class_(m, "CopyOnlyDeletedMove") + .def_readonly("value", &CopyOnlyDeletedMove::value); + m.def("get_copy_only_deleted_move", []() { return CopyOnlyDeletedMove(42); }); +#endif + // test_move_and_copy_casts // NOLINTNEXTLINE(performance-unnecessary-value-param) m.def("move_and_copy_casts", [](const py::object &o) { diff --git a/tests/test_copy_move.py b/tests/test_copy_move.py index d843793350..c032797b1b 100644 --- a/tests/test_copy_move.py +++ b/tests/test_copy_move.py @@ -142,3 +142,14 @@ def test_unusual_op_ref(): # Merely to test that this still exists and built successfully. assert m.CallCastUnusualOpRefConstRef().__class__.__name__ == "UnusualOpRef" assert m.CallCastUnusualOpRefMovable().__class__.__name__ == "UnusualOpRef" + + +@pytest.mark.skipif( + not hasattr(m, "get_copy_only_deleted_move"), reason="requires C++17 copy elision" +) +def test_copy_only_deleted_move(): + """#6142: a copyable type with a deleted move constructor can be returned by value + + This is primarily a compile-time regression test. + """ + assert m.get_copy_only_deleted_move().value == 42 From f3c06be11d10287719cb84fc8458347e8bf4cfad Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 14 Aug 2026 11:45:41 -0400 Subject: [PATCH 2/2] refactor: guard with __cpp_guaranteed_copy_elision Per review: the feature-test macro expresses the intent more precisely than PYBIND11_CPP17. Assisted-by: ClaudeCode:claude-opus-5 --- include/pybind11/detail/function_ref.h | 8 ++++---- tests/test_copy_move.cpp | 6 +++--- tests/test_copy_move.py | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/include/pybind11/detail/function_ref.h b/include/pybind11/detail/function_ref.h index c70e4ae15f..f5f9d7aead 100644 --- a/include/pybind11/detail/function_ref.h +++ b/include/pybind11/detail/function_ref.h @@ -31,7 +31,7 @@ // - renamed back to function_ref // - use pybind11 enable_if_t, remove_cvref_t, and remove_reference_t // - lint suppressions -// - accept same-type non-movable returns under C++17 guaranteed copy elision +// - accept same-type non-movable returns under guaranteed copy elision // (issue #6142) // torch::executor: modified from llvm::function_ref @@ -59,11 +59,11 @@ class function_ref; // pybind11: is_convertible is false for a copyable but non-movable // type (it tests conversion from an xvalue, which selects the deleted move -// constructor), but in C++17 a same-type prvalue is returnable via guaranteed -// copy elision. Accept that case explicitly. See issue #6142. +// constructor), but with guaranteed copy elision a same-type prvalue is still +// returnable. Accept that case explicitly. See issue #6142. template using is_returnable_as = bool_constant< -#if defined(PYBIND11_CPP17) +#if defined(__cpp_guaranteed_copy_elision) && __cpp_guaranteed_copy_elision >= 201606L std::is_same::value || #endif std::is_convertible::value>; diff --git a/tests/test_copy_move.cpp b/tests/test_copy_move.cpp index cd1c27215c..ed1bf61e95 100644 --- a/tests/test_copy_move.cpp +++ b/tests/test_copy_move.cpp @@ -109,8 +109,8 @@ class CopyOnlyInt { }; // #6142: returning a copy-constructible type with an explicitly deleted move // constructor failed to compile inside detail::function_ref (3.1.0 regression). -// Returning such a type by value requires C++17 guaranteed copy elision. -#if defined(PYBIND11_CPP17) +// Returning such a type by value requires guaranteed copy elision. +#if defined(__cpp_guaranteed_copy_elision) && __cpp_guaranteed_copy_elision >= 201606L class CopyOnlyDeletedMove { public: explicit CopyOnlyDeletedMove(int v) : value{v} {} @@ -193,7 +193,7 @@ TEST_SUBMODULE(copy_move_policies, m) { .def_static("get_one", &lacking_move_ctor::get_one, py::return_value_policy::move); // test_copy_only_deleted_move (#6142) -#if defined(PYBIND11_CPP17) +#if defined(__cpp_guaranteed_copy_elision) && __cpp_guaranteed_copy_elision >= 201606L py::class_(m, "CopyOnlyDeletedMove") .def_readonly("value", &CopyOnlyDeletedMove::value); m.def("get_copy_only_deleted_move", []() { return CopyOnlyDeletedMove(42); }); diff --git a/tests/test_copy_move.py b/tests/test_copy_move.py index c032797b1b..1c3094f417 100644 --- a/tests/test_copy_move.py +++ b/tests/test_copy_move.py @@ -145,7 +145,8 @@ def test_unusual_op_ref(): @pytest.mark.skipif( - not hasattr(m, "get_copy_only_deleted_move"), reason="requires C++17 copy elision" + not hasattr(m, "get_copy_only_deleted_move"), + reason="requires guaranteed copy elision", ) def test_copy_only_deleted_move(): """#6142: a copyable type with a deleted move constructor can be returned by value