Skip to content

Commit 89ba589

Browse files
committed
fix: returning a copyable type with a deleted move constructor (#6142)
detail::function_ref's constructor SFINAE required is_convertible<Ret, Ret>, 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
1 parent 856a348 commit 89ba589

3 files changed

Lines changed: 49 additions & 2 deletions

File tree

include/pybind11/detail/function_ref.h

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
// - renamed back to function_ref
3232
// - use pybind11 enable_if_t, remove_cvref_t, and remove_reference_t
3333
// - lint suppressions
34+
// - accept same-type non-movable returns under C++17 guaranteed copy elision
35+
// (issue #6142)
3436

3537
// torch::executor: modified from llvm::function_ref
3638
// - renamed to FunctionRef
@@ -55,6 +57,17 @@ PYBIND11_NAMESPACE_BEGIN(detail)
5557
template <typename Fn>
5658
class function_ref;
5759

60+
// pybind11: is_convertible<Ret, Ret> is false for a copyable but non-movable
61+
// type (it tests conversion from an xvalue, which selects the deleted move
62+
// constructor), but in C++17 a same-type prvalue is returnable via guaranteed
63+
// copy elision. Accept that case explicitly. See issue #6142.
64+
template <typename From, typename To>
65+
using is_returnable_as = bool_constant<
66+
#if defined(PYBIND11_CPP17)
67+
std::is_same<From, To>::value ||
68+
#endif
69+
std::is_convertible<From, To>::value>;
70+
5871
template <typename Ret, typename... Params>
5972
class function_ref<Ret(Params...)> {
6073
Ret (*callback)(intptr_t callable, Params... params) = nullptr;
@@ -81,8 +94,8 @@ class function_ref<Ret(Params...)> {
8194
// Functor must be callable and return a suitable type.
8295
enable_if_t<
8396
std::is_void<Ret>::value
84-
|| std::is_convertible<decltype(std::declval<Callable>()(std::declval<Params>()...)),
85-
Ret>::value> * = nullptr)
97+
|| is_returnable_as<decltype(std::declval<Callable>()(std::declval<Params>()...)),
98+
Ret>::value> * = nullptr)
8699
: callback(callback_fn<remove_reference_t<Callable>>),
87100
callable(reinterpret_cast<intptr_t>(&callable)) {}
88101

tests/test_copy_move.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,22 @@ class CopyOnlyInt {
107107

108108
int value;
109109
};
110+
// #6142: returning a copy-constructible type with an explicitly deleted move
111+
// constructor failed to compile inside detail::function_ref (3.1.0 regression).
112+
// Returning such a type by value requires C++17 guaranteed copy elision.
113+
#if defined(PYBIND11_CPP17)
114+
class CopyOnlyDeletedMove {
115+
public:
116+
explicit CopyOnlyDeletedMove(int v) : value{v} {}
117+
CopyOnlyDeletedMove(const CopyOnlyDeletedMove &) = default;
118+
CopyOnlyDeletedMove &operator=(const CopyOnlyDeletedMove &) = delete;
119+
CopyOnlyDeletedMove(CopyOnlyDeletedMove &&) = delete;
120+
CopyOnlyDeletedMove &operator=(CopyOnlyDeletedMove &&) = delete;
121+
122+
int value;
123+
};
124+
#endif
125+
110126
PYBIND11_NAMESPACE_BEGIN(pybind11)
111127
PYBIND11_NAMESPACE_BEGIN(detail)
112128
template <>
@@ -176,6 +192,13 @@ TEST_SUBMODULE(copy_move_policies, m) {
176192
py::class_<lacking_move_ctor>(m, "lacking_move_ctor")
177193
.def_static("get_one", &lacking_move_ctor::get_one, py::return_value_policy::move);
178194

195+
// test_copy_only_deleted_move (#6142)
196+
#if defined(PYBIND11_CPP17)
197+
py::class_<CopyOnlyDeletedMove>(m, "CopyOnlyDeletedMove")
198+
.def_readonly("value", &CopyOnlyDeletedMove::value);
199+
m.def("get_copy_only_deleted_move", []() { return CopyOnlyDeletedMove(42); });
200+
#endif
201+
179202
// test_move_and_copy_casts
180203
// NOLINTNEXTLINE(performance-unnecessary-value-param)
181204
m.def("move_and_copy_casts", [](const py::object &o) {

tests/test_copy_move.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,14 @@ def test_unusual_op_ref():
142142
# Merely to test that this still exists and built successfully.
143143
assert m.CallCastUnusualOpRefConstRef().__class__.__name__ == "UnusualOpRef"
144144
assert m.CallCastUnusualOpRefMovable().__class__.__name__ == "UnusualOpRef"
145+
146+
147+
@pytest.mark.skipif(
148+
not hasattr(m, "get_copy_only_deleted_move"), reason="requires C++17 copy elision"
149+
)
150+
def test_copy_only_deleted_move():
151+
"""#6142: a copyable type with a deleted move constructor can be returned by value
152+
153+
This is primarily a compile-time regression test.
154+
"""
155+
assert m.get_copy_only_deleted_move().value == 42

0 commit comments

Comments
 (0)