diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index d1ab6deeb4..a95e8c1ff4 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -1056,14 +1056,30 @@ struct strip_function_object { using type = typename remove_class::type; }; +// Strip noexcept from a free function type (C++17: noexcept is part of the type). +template +struct remove_noexcept { + using type = T; +}; +#ifdef __cpp_noexcept_function_type +template +struct remove_noexcept { + using type = R(A...); +}; +#endif +template +using remove_noexcept_t = typename remove_noexcept::type; + // Extracts the function signature from a function, function pointer or lambda. +// Strips noexcept from the result so that factory/pickle_factory partial specializations +// (which match plain Return(Args...)) work correctly with noexcept callables (issue #2234). template > -using function_signature_t = conditional_t< +using function_signature_t = remove_noexcept_t::value, F, typename conditional_t::value || std::is_member_pointer::value, std::remove_pointer, - strip_function_object>::type>; + strip_function_object>::type>>; /// Returns true if the type looks like a lambda: that is, isn't a function, pointer or member /// pointer. Note that this can catch all sorts of other things, too; this is intended to be used @@ -1212,6 +1228,25 @@ struct overload_cast_impl { -> decltype(pmf) { return pmf; } + +#ifdef __cpp_noexcept_function_type + template + constexpr auto operator()(Return (*pf)(Args...) noexcept) const noexcept -> decltype(pf) { + return pf; + } + + template + constexpr auto operator()(Return (Class::*pmf)(Args...) noexcept, + std::false_type = {}) const noexcept -> decltype(pmf) { + return pmf; + } + + template + constexpr auto operator()(Return (Class::*pmf)(Args...) const noexcept, + std::true_type) const noexcept -> decltype(pmf) { + return pmf; + } +#endif }; PYBIND11_NAMESPACE_END(detail) diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h index 408d1699cf..547abbdeed 100644 --- a/include/pybind11/numpy.h +++ b/include/pybind11/numpy.h @@ -2327,4 +2327,32 @@ Helper vectorize(Return (Class::*f)(Args...) const) { return Helper(std::mem_fn(f)); } +#ifdef __cpp_noexcept_function_type +// Vectorize a class method (non-const, noexcept): +template ())), + Return, + Class *, + Args...>> +Helper vectorize(Return (Class::*f)(Args...) noexcept) { + return Helper(std::mem_fn(f)); +} + +// Vectorize a class method (const, noexcept): +template ())), + Return, + const Class *, + Args...>> +Helper vectorize(Return (Class::*f)(Args...) const noexcept) { + return Helper(std::mem_fn(f)); +} +#endif + PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 76d998a3ba..dc72137eeb 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -370,6 +370,48 @@ class cpp_function : public function { extra...); } +#ifdef __cpp_noexcept_function_type + /// Construct a cpp_function from a class method (non-const, no ref-qualifier, noexcept) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) noexcept, const Extra &...extra) { + initialize( + [f](Class *c, Arg... args) -> Return { return (c->*f)(std::forward(args)...); }, + (Return (*)(Class *, Arg...)) nullptr, + extra...); + } + + /// Construct a cpp_function from a class method (non-const, lvalue ref-qualifier, noexcept) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) & noexcept, const Extra &...extra) { + initialize( + [f](Class *c, Arg... args) -> Return { return (c->*f)(std::forward(args)...); }, + (Return (*)(Class *, Arg...)) nullptr, + extra...); + } + + /// Construct a cpp_function from a class method (const, no ref-qualifier, noexcept) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) const noexcept, const Extra &...extra) { + initialize([f](const Class *c, + Arg... args) -> Return { return (c->*f)(std::forward(args)...); }, + (Return (*)(const Class *, Arg...)) nullptr, + extra...); + } + + /// Construct a cpp_function from a class method (const, lvalue ref-qualifier, noexcept) + template + // NOLINTNEXTLINE(google-explicit-constructor) + cpp_function(Return (Class::*f)(Arg...) const & noexcept, const Extra &...extra) { + initialize([f](const Class *c, + Arg... args) -> Return { return (c->*f)(std::forward(args)...); }, + (Return (*)(const Class *, Arg...)) nullptr, + extra...); + } +#endif + /// Return the function name object name() const { return attr("__name__"); } @@ -1905,6 +1947,61 @@ auto method_adaptor(Return (Class::*pmf)(Args...) const) -> Return (Derived::*)( return pmf; } +template +auto method_adaptor(Return (Class::*pmf)(Args...) &) -> Return (Derived::*)(Args...) & { + static_assert( + detail::is_accessible_base_of::value, + "Cannot bind an inaccessible base class method; use a lambda definition instead"); + return pmf; +} + +template +auto method_adaptor(Return (Class::*pmf)(Args...) const &) + -> Return (Derived::*)(Args...) const & { + static_assert( + detail::is_accessible_base_of::value, + "Cannot bind an inaccessible base class method; use a lambda definition instead"); + return pmf; +} + +#ifdef __cpp_noexcept_function_type +template +auto method_adaptor(Return (Class::*pmf)(Args...) noexcept) + -> Return (Derived::*)(Args...) noexcept { + static_assert( + detail::is_accessible_base_of::value, + "Cannot bind an inaccessible base class method; use a lambda definition instead"); + return pmf; +} + +template +auto method_adaptor(Return (Class::*pmf)(Args...) const noexcept) + -> Return (Derived::*)(Args...) const noexcept { + static_assert( + detail::is_accessible_base_of::value, + "Cannot bind an inaccessible base class method; use a lambda definition instead"); + return pmf; +} + +template +auto method_adaptor(Return (Class::*pmf)(Args...) & noexcept) + -> Return (Derived::*)(Args...) & noexcept { + static_assert( + detail::is_accessible_base_of::value, + "Cannot bind an inaccessible base class method; use a lambda definition instead"); + return pmf; +} + +template +auto method_adaptor(Return (Class::*pmf)(Args...) const & noexcept) + -> Return (Derived::*)(Args...) const & noexcept { + static_assert( + detail::is_accessible_base_of::value, + "Cannot bind an inaccessible base class method; use a lambda definition instead"); + return pmf; +} +#endif + PYBIND11_NAMESPACE_BEGIN(detail) // Helper for the property_cpp_function static member functions below. @@ -2361,6 +2458,18 @@ class class_ : public detail::generic_type { return def_buffer([func](const type &obj) { return (obj.*func)(); }); } +#ifdef __cpp_noexcept_function_type + template + class_ &def_buffer(Return (Class::*func)(Args...) noexcept) { + return def_buffer([func](type &obj) { return (obj.*func)(); }); + } + + template + class_ &def_buffer(Return (Class::*func)(Args...) const noexcept) { + return def_buffer([func](const type &obj) { return (obj.*func)(); }); + } +#endif + template class_ &def_readwrite(const char *name, D C::*pm, const Extra &...extra) { static_assert(std::is_same::value || std::is_base_of::value, diff --git a/tests/test_buffers.cpp b/tests/test_buffers.cpp index 525be80bc0..f0ac8af4b4 100644 --- a/tests/test_buffers.cpp +++ b/tests/test_buffers.cpp @@ -439,4 +439,50 @@ TEST_SUBMODULE(buffers, m) { PyBuffer_Release(&buffer); return result; }); + + // test_noexcept_def_buffer (issue #2234) + // def_buffer(Return (Class::*)(Args...) noexcept) and + // def_buffer(Return (Class::*)(Args...) const noexcept) must compile and work correctly. + struct OneDBuffer { + // Declare m_data before m_n to match initialiser list order below. + float *m_data; + py::ssize_t m_n; + explicit OneDBuffer(py::ssize_t n) : m_data(new float[(size_t) n]()), m_n(n) {} + ~OneDBuffer() { delete[] m_data; } + // Exercises def_buffer(Return (Class::*)(Args...) noexcept) + py::buffer_info get_buffer() noexcept { + return py::buffer_info(m_data, + sizeof(float), + py::format_descriptor::format(), + 1, + {m_n}, + {(py::ssize_t) sizeof(float)}); + } + }; + + // non-const noexcept member function form + py::class_(m, "OneDBuffer", py::buffer_protocol()) + .def(py::init()) + .def_buffer(&OneDBuffer::get_buffer); + + // const noexcept member function form (separate class to avoid ambiguity) + struct OneDBufferConst { + float *m_data; + py::ssize_t m_n; + explicit OneDBufferConst(py::ssize_t n) : m_data(new float[(size_t) n]()), m_n(n) {} + ~OneDBufferConst() { delete[] m_data; } + // Exercises def_buffer(Return (Class::*)(Args...) const noexcept) + py::buffer_info get_buffer() const noexcept { + return py::buffer_info(m_data, + sizeof(float), + py::format_descriptor::format(), + 1, + {m_n}, + {(py::ssize_t) sizeof(float)}, + /*readonly=*/true); + } + }; + py::class_(m, "OneDBufferConst", py::buffer_protocol()) + .def(py::init()) + .def_buffer(&OneDBufferConst::get_buffer); } diff --git a/tests/test_buffers.py b/tests/test_buffers.py index f666df5bad..cc0bbf75d1 100644 --- a/tests/test_buffers.py +++ b/tests/test_buffers.py @@ -399,3 +399,25 @@ def check_strides(mat): m.get_py_buffer(dmat, m.PyBUF_ANY_CONTIGUOUS) with pytest.raises(expected_exception): m.get_py_buffer(dmat, m.PyBUF_F_CONTIGUOUS) + + +def test_noexcept_def_buffer(): + """Test issue #2234: def_buffer with noexcept member function pointers. + + Covers both new def_buffer specialisations: + - def_buffer(Return (Class::*)(Args...) noexcept) + - def_buffer(Return (Class::*)(Args...) const noexcept) + """ + # non-const noexcept member function form + buf = m.OneDBuffer(5) + arr = np.frombuffer(buf, dtype=np.float32) + assert arr.shape == (5,) + arr[2] = 3.14 + arr2 = np.frombuffer(buf, dtype=np.float32) + assert arr2[2] == pytest.approx(3.14) + + # const noexcept member function form + cbuf = m.OneDBufferConst(4) + carr = np.frombuffer(cbuf, dtype=np.float32) + assert carr.shape == (4,) + assert carr.flags["WRITEABLE"] is False diff --git a/tests/test_methods_and_attributes.cpp b/tests/test_methods_and_attributes.cpp index f5fb02d121..e14d5f09bd 100644 --- a/tests/test_methods_and_attributes.cpp +++ b/tests/test_methods_and_attributes.cpp @@ -161,6 +161,39 @@ class RegisteredDerived : public UnregisteredBase { double sum() const { return rw_value + ro_value; } }; +// Issue #2234: noexcept methods in an unregistered base should be bindable on the derived class. +// In C++17, noexcept is part of the function type, so &Derived::method resolves to +// a Base member function pointer with noexcept, requiring explicit template specializations. +class NoexceptUnregisteredBase { +public: + // Exercises cpp_function(Return (Class::*)(Args...) const noexcept, ...) + int value() const noexcept { return m_value; } + // Exercises cpp_function(Return (Class::*)(Args...) noexcept, ...) + void set_value(int v) noexcept { m_value = v; } + // Exercises cpp_function(Return (Class::*)(Args...) & noexcept, ...) + void increment() & noexcept { ++m_value; } + // Exercises cpp_function(Return (Class::*)(Args...) const & noexcept, ...) + int capped_value() const & noexcept { return m_value < 100 ? m_value : 100; } + +private: + int m_value = 99; +}; +class NoexceptDerived : public NoexceptUnregisteredBase { +public: + using NoexceptUnregisteredBase::NoexceptUnregisteredBase; +}; + +// Exercises overload_cast with noexcept member function pointers (issue #2234). +// In C++17, overload_cast must have noexcept variants to resolve noexcept overloads. +struct NoexceptOverloaded { + py::str method(int) noexcept { return "(int)"; } + py::str method(int) const noexcept { return "(int) const"; } + py::str method(float) noexcept { return "(float)"; } +}; +// Exercises overload_cast with noexcept free function pointers. +int noexcept_free_func(int x) noexcept { return x + 1; } +int noexcept_free_func(float x) noexcept { return static_cast(x) + 2; } + // Test explicit lvalue ref-qualification struct RefQualified { int value = 0; @@ -474,6 +507,61 @@ TEST_SUBMODULE(methods_and_attributes, m) { = decltype(py::method_adaptor(&RegisteredDerived::do_nothing)); static_assert(std::is_same::value, ""); + // test_noexcept_base (issue #2234) + // In C++17, noexcept is part of the function type. Binding a noexcept method from an + // unregistered base class must resolve `self` to the derived type, not the base type. + py::class_(m, "NoexceptDerived") + .def(py::init<>()) + // cpp_function(Return (Class::*)(Args...) const noexcept, ...) + .def("value", &NoexceptDerived::value) + // cpp_function(Return (Class::*)(Args...) noexcept, ...) + .def("set_value", &NoexceptDerived::set_value) + // cpp_function(Return (Class::*)(Args...) & noexcept, ...) + .def("increment", &NoexceptDerived::increment) + // cpp_function(Return (Class::*)(Args...) const & noexcept, ...) + .def("capped_value", &NoexceptDerived::capped_value); + +#ifdef __cpp_noexcept_function_type + // method_adaptor must also handle noexcept member function pointers (issue #2234). + // Verify the noexcept specifier is preserved in the resulting Derived pointer type. + using AdaptedConstNoexcept + = decltype(py::method_adaptor(&NoexceptDerived::value)); + static_assert( + std::is_same::value, ""); + using AdaptedNoexcept + = decltype(py::method_adaptor(&NoexceptDerived::set_value)); + static_assert(std::is_same::value, + ""); +#endif + + // test_noexcept_overload_cast (issue #2234) + // overload_cast must have noexcept operator() overloads so it can resolve noexcept methods. +#ifdef PYBIND11_OVERLOAD_CAST + py::class_(m, "NoexceptOverloaded") + .def(py::init<>()) + // overload_cast_impl::operator()(Return (Class::*)(Args...) noexcept, false_type) + .def("method", py::overload_cast(&NoexceptOverloaded::method)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) const noexcept, true_type) + .def("method_const", py::overload_cast(&NoexceptOverloaded::method, py::const_)) + // overload_cast_impl::operator()(Return (Class::*)(Args...) noexcept, false_type) float + .def("method_float", py::overload_cast(&NoexceptOverloaded::method)); + // overload_cast_impl::operator()(Return (*)(Args...) noexcept) + m.def("noexcept_free_func", py::overload_cast(noexcept_free_func)); + m.def("noexcept_free_func_float", py::overload_cast(noexcept_free_func)); +#else + // Fallback using explicit static_cast for C++11/14 + py::class_(m, "NoexceptOverloaded") + .def(py::init<>()) + .def("method", + static_cast(&NoexceptOverloaded::method)) + .def("method_const", + static_cast(&NoexceptOverloaded::method)) + .def("method_float", + static_cast(&NoexceptOverloaded::method)); + m.def("noexcept_free_func", static_cast(noexcept_free_func)); + m.def("noexcept_free_func_float", static_cast(noexcept_free_func)); +#endif + // test_methods_and_attributes py::class_(m, "RefQualified") .def(py::init<>()) diff --git a/tests/test_methods_and_attributes.py b/tests/test_methods_and_attributes.py index 553d5bfc1b..23a0663638 100644 --- a/tests/test_methods_and_attributes.py +++ b/tests/test_methods_and_attributes.py @@ -517,6 +517,52 @@ def test_unregistered_base_implementations(): assert a.ro_value_prop == 1.75 +def test_noexcept_base(): + """Test issue #2234: binding noexcept methods inherited from an unregistered base class. + + In C++17 noexcept is part of the function type, so &Derived::noexcept_method resolves + to a Base member-function pointer with noexcept specifier. pybind11 must use the Derived + type as `self`, not the Base type, otherwise the call raises TypeError at runtime. + + Covers all four new cpp_function constructor specialisations: + - Return (Class::*)(Args...) noexcept (set_value) + - Return (Class::*)(Args...) const noexcept (value) + - Return (Class::*)(Args...) & noexcept (increment) + - Return (Class::*)(Args...) const & noexcept (capped_value) + """ + obj = m.NoexceptDerived() + # const noexcept + assert obj.value() == 99 + # noexcept (non-const) + obj.set_value(7) + assert obj.value() == 7 + # & noexcept (non-const lvalue ref-qualified) + obj.increment() + assert obj.value() == 8 + # const & noexcept (const lvalue ref-qualified) + assert obj.capped_value() == 8 + obj.set_value(200) + assert obj.capped_value() == 100 # capped at 100 + + +def test_noexcept_overload_cast(): + """Test issue #2234: overload_cast must handle noexcept member and free function pointers. + + In C++17 noexcept is part of the function type, so overload_cast_impl needs dedicated + operator() overloads for noexcept free functions and non-const/const member functions. + """ + obj = m.NoexceptOverloaded() + # overload_cast_impl::operator()(Return (Class::*)(Args...) noexcept, false_type) + assert obj.method(1) == "(int)" + # overload_cast_impl::operator()(Return (Class::*)(Args...) const noexcept, true_type) + assert obj.method_const(2) == "(int) const" + # overload_cast_impl::operator()(Return (Class::*)(Args...) noexcept, false_type) float + assert obj.method_float(3.0) == "(float)" + # overload_cast_impl::operator()(Return (*)(Args...) noexcept) + assert m.noexcept_free_func(10) == 11 + assert m.noexcept_free_func_float(10.0) == 12 + + def test_ref_qualified(): """Tests that explicit lvalue ref-qualified methods can be called just like their non ref-qualified counterparts.""" diff --git a/tests/test_numpy_vectorize.cpp b/tests/test_numpy_vectorize.cpp index dcc4c6ac25..d3412ce452 100644 --- a/tests/test_numpy_vectorize.cpp +++ b/tests/test_numpy_vectorize.cpp @@ -78,6 +78,13 @@ TEST_SUBMODULE(numpy_vectorize, m) { struct VectorizeTestClass { explicit VectorizeTestClass(int v) : value{v} {}; float method(int x, float y) const { return y + (float) (x + value); } + // Exercises vectorize(Return (Class::*)(Args...) noexcept) + // NOLINTNEXTLINE(readability-make-member-function-const) + float method_noexcept(int x, float y) noexcept { return y + (float) (x + value); } + // Exercises vectorize(Return (Class::*)(Args...) const noexcept) + float method_const_noexcept(int x, float y) const noexcept { + return y + (float) (x + value); + } int value = 0; }; py::class_ vtc(m, "VectorizeTestClass"); @@ -85,6 +92,8 @@ TEST_SUBMODULE(numpy_vectorize, m) { // Automatic vectorizing of methods vtc.def("method", py::vectorize(&VectorizeTestClass::method)); + vtc.def("method_noexcept", py::vectorize(&VectorizeTestClass::method_noexcept)); + vtc.def("method_const_noexcept", py::vectorize(&VectorizeTestClass::method_const_noexcept)); // test_trivial_broadcasting // Internal optimization test for whether the input is trivially broadcastable: diff --git a/tests/test_numpy_vectorize.py b/tests/test_numpy_vectorize.py index 05f7c704f5..eb02603c98 100644 --- a/tests/test_numpy_vectorize.py +++ b/tests/test_numpy_vectorize.py @@ -246,6 +246,22 @@ def test_method_vectorization(): assert np.all(o.method(x, y) == [[14, 15], [24, 25]]) +def test_noexcept_method_vectorization(): + """Test issue #2234: vectorize must handle noexcept member function pointers. + + Covers both new vectorize specialisations: + - vectorize(Return (Class::*)(Args...) noexcept) + - vectorize(Return (Class::*)(Args...) const noexcept) + """ + o = m.VectorizeTestClass(3) + x = np.array([1, 2], dtype="int") + y = np.array([[10], [20]], dtype="float32") + # vectorize(Return (Class::*)(Args...) noexcept) + assert np.all(o.method_noexcept(x, y) == [[14, 15], [24, 25]]) + # vectorize(Return (Class::*)(Args...) const noexcept) + assert np.all(o.method_const_noexcept(x, y) == [[14, 15], [24, 25]]) + + def test_array_collapse(): assert not isinstance(m.vectorized_func(1, 2, 3), np.ndarray) assert not isinstance(m.vectorized_func(np.array(1), 2, 3), np.ndarray)