Skip to content

Commit 1d81191

Browse files
authored
Disable implicit conversion of 0 to pybind11::handle. (#4008)
* Disable implicit conversion from `0` to `pybind11::handle`. * Reverse or-ed condition in an attempt to resolve GCC 8.3.0 errors (i386/debian:buster). * Trying the simpler `std::is_same<T, PyObject *>` * Add implicit_conversion_from_pytorch_THPObjectPtr_to_handle test. * Accommodate types with implicit conversions to `PyObject *`, other than `handle` & `handle` subclasses, or integral types. * Fix copy-paste mishap (picked wrong name). * Revamp SFINAE construct to actually fix the pytorch issue (already validated against pytorch proper). The first version of the reduced pytorch code was critically missing the move ctor. The first version of the accompanying test was meaningless. Note: It turns out the `!std::is_arithmetic<T>` condition is not needed: `int` is not in general implicitly convertible to `PyObject *`, only the literal `0` is. * Use `NOLINT(performance-noexcept-move-constructor)` for reduced code from the wild (rather than changing the code). * Use any_of, all_of, negation. It turns out to clang-format nicer. * Clean up comments for changed code. * Reduce pytorch situation further, add test for operator ... const. * Use `none_of` as suggested by @Skylion007 * Add `pure_compile_tests_for_handle_from_PyObject_pointers()` * Fix inconsequential oversight (retested). * Factor our `is_pyobj_ptr_or_nullptr_t` to make the SFINAE conditions more readable. * Remove stray line (oversight). * Make the `pure_compile_tests_for_handle_from_PyObject_pointers()` "rhs-const-complete", too. * Remove the temporary PYBIND11_UNDO_PR4008 `#ifdef`.
1 parent bc9315f commit 1d81191

File tree

3 files changed

+87
-2
lines changed

3 files changed

+87
-2
lines changed

include/pybind11/pytypes.h

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,11 @@ class object_api : public pyobject_tag {
190190
bool rich_compare(object_api const &other, int value) const;
191191
};
192192

193+
template <typename T>
194+
using is_pyobj_ptr_or_nullptr_t = detail::any_of<std::is_same<T, PyObject *>,
195+
std::is_same<T, PyObject *const>,
196+
std::is_same<T, std::nullptr_t>>;
197+
193198
PYBIND11_NAMESPACE_END(detail)
194199

195200
#if !defined(PYBIND11_HANDLE_REF_DEBUG) && !defined(NDEBUG)
@@ -211,9 +216,23 @@ class handle : public detail::object_api<handle> {
211216
public:
212217
/// The default constructor creates a handle with a ``nullptr``-valued pointer
213218
handle() = default;
214-
/// Creates a ``handle`` from the given raw Python object pointer
219+
220+
/// Enable implicit conversion from ``PyObject *`` and ``nullptr``.
221+
/// Not using ``handle(PyObject *ptr)`` to avoid implicit conversion from ``0``.
222+
template <typename T,
223+
detail::enable_if_t<detail::is_pyobj_ptr_or_nullptr_t<T>::value, int> = 0>
224+
// NOLINTNEXTLINE(google-explicit-constructor)
225+
handle(T ptr) : m_ptr(ptr) {}
226+
227+
/// Enable implicit conversion through ``T::operator PyObject *()``.
228+
template <
229+
typename T,
230+
detail::enable_if_t<detail::all_of<detail::none_of<std::is_base_of<handle, T>,
231+
detail::is_pyobj_ptr_or_nullptr_t<T>>,
232+
std::is_convertible<T, PyObject *>>::value,
233+
int> = 0>
215234
// NOLINTNEXTLINE(google-explicit-constructor)
216-
handle(PyObject *ptr) : m_ptr(ptr) {} // Allow implicit conversion from PyObject*
235+
handle(T &obj) : m_ptr(obj) {}
217236

218237
/// Return the underlying ``PyObject *`` pointer
219238
PyObject *ptr() const { return m_ptr; }

tests/test_pytypes.cpp

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,68 @@ class float_ : public py::object {
3939
};
4040
} // namespace external
4141

42+
namespace implicit_conversion_from_0_to_handle {
43+
// Uncomment to trigger compiler error. Note: Before PR #4008 this used to compile successfully.
44+
// void expected_to_trigger_compiler_error() { py::handle(0); }
45+
} // namespace implicit_conversion_from_0_to_handle
46+
47+
// Used to validate systematically that PR #4008 does/did NOT change the behavior.
48+
void pure_compile_tests_for_handle_from_PyObject_pointers() {
49+
{
50+
PyObject *ptr = Py_None;
51+
py::handle{ptr};
52+
}
53+
{
54+
PyObject *const ptr = Py_None;
55+
py::handle{ptr};
56+
}
57+
// Uncomment to trigger compiler errors.
58+
// PyObject const * ptr = Py_None; py::handle{ptr};
59+
// PyObject const *const ptr = Py_None; py::handle{ptr};
60+
// PyObject volatile * ptr = Py_None; py::handle{ptr};
61+
// PyObject volatile *const ptr = Py_None; py::handle{ptr};
62+
// PyObject const volatile * ptr = Py_None; py::handle{ptr};
63+
// PyObject const volatile *const ptr = Py_None; py::handle{ptr};
64+
}
65+
66+
namespace handle_from_move_only_type_with_operator_PyObject {
67+
68+
// Reduced from
69+
// https://github.com/pytorch/pytorch/blob/279634f384662b7c3a9f8bf7ccc3a6afd2f05657/torch/csrc/utils/object_ptr.h
70+
struct operator_ncnst {
71+
operator_ncnst() = default;
72+
operator_ncnst(operator_ncnst &&) = default;
73+
operator PyObject *() /* */ { return Py_None; } // NOLINT(google-explicit-constructor)
74+
};
75+
76+
struct operator_const {
77+
operator_const() = default;
78+
operator_const(operator_const &&) = default;
79+
operator PyObject *() const { return Py_None; } // NOLINT(google-explicit-constructor)
80+
};
81+
82+
bool from_ncnst() {
83+
operator_ncnst obj;
84+
auto h = py::handle(obj); // Critical part of test: does this compile?
85+
return h.ptr() == Py_None; // Just something.
86+
}
87+
88+
bool from_const() {
89+
operator_const obj;
90+
auto h = py::handle(obj); // Critical part of test: does this compile?
91+
return h.ptr() == Py_None; // Just something.
92+
}
93+
94+
void m_defs(py::module_ &m) {
95+
m.def("handle_from_move_only_type_with_operator_PyObject_ncnst", from_ncnst);
96+
m.def("handle_from_move_only_type_with_operator_PyObject_const", from_const);
97+
}
98+
99+
} // namespace handle_from_move_only_type_with_operator_PyObject
100+
42101
TEST_SUBMODULE(pytypes, m) {
102+
handle_from_move_only_type_with_operator_PyObject::m_defs(m);
103+
43104
// test_bool
44105
m.def("get_bool", [] { return py::bool_(false); });
45106
// test_int

tests/test_pytypes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
from pybind11_tests import pytypes as m
1010

1111

12+
def test_handle_from_move_only_type_with_operator_PyObject(): # noqa: N802
13+
assert m.handle_from_move_only_type_with_operator_PyObject_ncnst()
14+
assert m.handle_from_move_only_type_with_operator_PyObject_const()
15+
16+
1217
def test_bool(doc):
1318
assert doc(m.get_bool) == "get_bool() -> bool"
1419

0 commit comments

Comments
 (0)