fix: bind noexcept and ref-qualified methods from unregistered base classes (#5992)

* Strip noexcept from cpp17 function type bindings

* Fix a bug and increase test coverage

* Does this fix it?

* Silence clang-tidy issue

* Simplify method adapter with macro and add missing rvalue adaptors + tests

* Supress clang-tidy errors

* Improve test coverage

* Add additional static assert

* Try to resolve MSVC C4003 warning

* Simplify method adaptor into 2 template instatiations with enable_if_t

* Fix ambiguous STL template

* Close remaining qualifier consistency gaps for member pointer bindings.

A production-code review after #2234 showed that ref-qualified member pointers were still inconsistently handled across def_buffer, vectorize, and overload_cast, so this adds the missing overloads with focused tests for each newly-supported signature.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Clarify why def_buffer/vectorize omit rvalue-qualified overloads.

These comments were added while reviewing the qualifier coverage follow-up, to document that buffer/vectorized calls operate on existing Python-owned instances and should not move-from self.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add compile-only overload_cast guard for ref-qualified methods.

This was added as a maintenance follow-up to the qualifier-consistency work, so future changes that introduce overload_cast ambiguity or wrong ref/noexcept resolution fail at compile time.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Refactor overload_cast_impl qualifier overloads with a macro.

As part of the qualifier-consistency maintenance follow-up, this reduces duplication in overload_cast_impl while preserving the same ref/noexcept coverage and keeping pedantic-clean macro expansion.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Expose __cpp_noexcept_function_type to Python tests and use explicit skip guards.

This replaces hasattr-based optional assertions with skipif-gated noexcept-only tests so skipped coverage is visible in pytest output while keeping non-noexcept checks always active.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add static_assert in method_adaptor to guard that T is a member function pointer.

Suggested by @Skylion007 in PR #5992 review comment [T007].

Made-with: Cursor

* automatic clang-format change (because of #6002)

---------

Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Aaron Gokaslan
2026-03-26 19:21:32 -04:00
committed by Ralf W. Grosse-Kunstleve
parent c0cfa96555
commit 3cb5a763c1
10 changed files with 1015 additions and 21 deletions

View File

@@ -439,4 +439,133 @@ 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<float>::format(),
1,
{m_n},
{(py::ssize_t) sizeof(float)});
}
};
// non-const noexcept member function form
py::class_<OneDBuffer>(m, "OneDBuffer", py::buffer_protocol())
.def(py::init<py::ssize_t>())
.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<float>::format(),
1,
{m_n},
{(py::ssize_t) sizeof(float)},
/*readonly=*/true);
}
};
py::class_<OneDBufferConst>(m, "OneDBufferConst", py::buffer_protocol())
.def(py::init<py::ssize_t>())
.def_buffer(&OneDBufferConst::get_buffer);
// test_ref_qualified_def_buffer
struct OneDBufferLRef {
float *m_data;
py::ssize_t m_n;
explicit OneDBufferLRef(py::ssize_t n) : m_data(new float[(size_t) n]()), m_n(n) {}
~OneDBufferLRef() { delete[] m_data; }
// Exercises def_buffer(Return (Class::*)(Args...) &)
py::buffer_info get_buffer() & {
return py::buffer_info(m_data,
sizeof(float),
py::format_descriptor<float>::format(),
1,
{m_n},
{(py::ssize_t) sizeof(float)});
}
};
py::class_<OneDBufferLRef>(m, "OneDBufferLRef", py::buffer_protocol())
.def(py::init<py::ssize_t>())
.def_buffer(&OneDBufferLRef::get_buffer);
struct OneDBufferConstLRef {
float *m_data;
py::ssize_t m_n;
explicit OneDBufferConstLRef(py::ssize_t n) : m_data(new float[(size_t) n]()), m_n(n) {}
~OneDBufferConstLRef() { delete[] m_data; }
// Exercises def_buffer(Return (Class::*)(Args...) const &)
py::buffer_info get_buffer() const & {
return py::buffer_info(m_data,
sizeof(float),
py::format_descriptor<float>::format(),
1,
{m_n},
{(py::ssize_t) sizeof(float)},
/*readonly=*/true);
}
};
py::class_<OneDBufferConstLRef>(m, "OneDBufferConstLRef", py::buffer_protocol())
.def(py::init<py::ssize_t>())
.def_buffer(&OneDBufferConstLRef::get_buffer);
#ifdef __cpp_noexcept_function_type
struct OneDBufferLRefNoexcept {
float *m_data;
py::ssize_t m_n;
explicit OneDBufferLRefNoexcept(py::ssize_t n) : m_data(new float[(size_t) n]()), m_n(n) {}
~OneDBufferLRefNoexcept() { 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<float>::format(),
1,
{m_n},
{(py::ssize_t) sizeof(float)});
}
};
py::class_<OneDBufferLRefNoexcept>(m, "OneDBufferLRefNoexcept", py::buffer_protocol())
.def(py::init<py::ssize_t>())
.def_buffer(&OneDBufferLRefNoexcept::get_buffer);
struct OneDBufferConstLRefNoexcept {
float *m_data;
py::ssize_t m_n;
explicit OneDBufferConstLRefNoexcept(py::ssize_t n)
: m_data(new float[(size_t) n]()), m_n(n) {}
~OneDBufferConstLRefNoexcept() { 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<float>::format(),
1,
{m_n},
{(py::ssize_t) sizeof(float)},
/*readonly=*/true);
}
};
py::class_<OneDBufferConstLRefNoexcept>(
m, "OneDBufferConstLRefNoexcept", py::buffer_protocol())
.def(py::init<py::ssize_t>())
.def_buffer(&OneDBufferConstLRefNoexcept::get_buffer);
#endif
}