Skip to content

Commit a7a3ed9

Browse files
committed
Fix a bug and increase test coverage
1 parent bd45ad0 commit a7a3ed9

File tree

7 files changed

+207
-5
lines changed

7 files changed

+207
-5
lines changed

include/pybind11/pybind11.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1965,6 +1965,24 @@ auto method_adaptor(Return (Class::*pmf)(Args...) const noexcept)
19651965
"Cannot bind an inaccessible base class method; use a lambda definition instead");
19661966
return pmf;
19671967
}
1968+
1969+
template <typename Derived, typename Return, typename Class, typename... Args>
1970+
auto method_adaptor(Return (Class::*pmf)(Args...) & noexcept)
1971+
-> Return (Derived::*)(Args...) & noexcept {
1972+
static_assert(
1973+
detail::is_accessible_base_of<Class, Derived>::value,
1974+
"Cannot bind an inaccessible base class method; use a lambda definition instead");
1975+
return pmf;
1976+
}
1977+
1978+
template <typename Derived, typename Return, typename Class, typename... Args>
1979+
auto method_adaptor(Return (Class::*pmf)(Args...) const & noexcept)
1980+
-> Return (Derived::*)(Args...) const & noexcept {
1981+
static_assert(
1982+
detail::is_accessible_base_of<Class, Derived>::value,
1983+
"Cannot bind an inaccessible base class method; use a lambda definition instead");
1984+
return pmf;
1985+
}
19681986
#endif
19691987

19701988
PYBIND11_NAMESPACE_BEGIN(detail)

tests/test_buffers.cpp

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,4 +439,50 @@ TEST_SUBMODULE(buffers, m) {
439439
PyBuffer_Release(&buffer);
440440
return result;
441441
});
442+
443+
// test_noexcept_def_buffer (issue #2234)
444+
// def_buffer(Return (Class::*)(Args...) noexcept) and
445+
// def_buffer(Return (Class::*)(Args...) const noexcept) must compile and work correctly.
446+
struct OneDBuffer {
447+
// Declare m_data before m_n to match initialiser list order below.
448+
float *m_data;
449+
py::ssize_t m_n;
450+
explicit OneDBuffer(py::ssize_t n) : m_data(new float[(size_t) n]()), m_n(n) {}
451+
~OneDBuffer() { delete[] m_data; }
452+
// Exercises def_buffer(Return (Class::*)(Args...) noexcept)
453+
py::buffer_info get_buffer() noexcept {
454+
return py::buffer_info(m_data,
455+
sizeof(float),
456+
py::format_descriptor<float>::format(),
457+
1,
458+
{m_n},
459+
{(py::ssize_t) sizeof(float)});
460+
}
461+
};
462+
463+
// non-const noexcept member function form
464+
py::class_<OneDBuffer>(m, "OneDBuffer", py::buffer_protocol())
465+
.def(py::init<py::ssize_t>())
466+
.def_buffer(&OneDBuffer::get_buffer);
467+
468+
// const noexcept member function form (separate class to avoid ambiguity)
469+
struct OneDBufferConst {
470+
float *m_data;
471+
py::ssize_t m_n;
472+
explicit OneDBufferConst(py::ssize_t n) : m_data(new float[(size_t) n]()), m_n(n) {}
473+
~OneDBufferConst() { delete[] m_data; }
474+
// Exercises def_buffer(Return (Class::*)(Args...) const noexcept)
475+
py::buffer_info get_buffer() const noexcept {
476+
return py::buffer_info(m_data,
477+
sizeof(float),
478+
py::format_descriptor<float>::format(),
479+
1,
480+
{m_n},
481+
{(py::ssize_t) sizeof(float)},
482+
/*readonly=*/true);
483+
}
484+
};
485+
py::class_<OneDBufferConst>(m, "OneDBufferConst", py::buffer_protocol())
486+
.def(py::init<py::ssize_t>())
487+
.def_buffer(&OneDBufferConst::get_buffer);
442488
}

tests/test_buffers.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,3 +399,27 @@ def check_strides(mat):
399399
m.get_py_buffer(dmat, m.PyBUF_ANY_CONTIGUOUS)
400400
with pytest.raises(expected_exception):
401401
m.get_py_buffer(dmat, m.PyBUF_F_CONTIGUOUS)
402+
403+
404+
def test_noexcept_def_buffer():
405+
"""Test issue #2234: def_buffer with noexcept member function pointers.
406+
407+
Covers both new def_buffer specialisations:
408+
- def_buffer(Return (Class::*)(Args...) noexcept)
409+
- def_buffer(Return (Class::*)(Args...) const noexcept)
410+
"""
411+
import numpy as np
412+
413+
# non-const noexcept member function form
414+
buf = m.OneDBuffer(5)
415+
arr = np.frombuffer(buf, dtype=np.float32)
416+
assert arr.shape == (5,)
417+
arr[2] = 3.14
418+
arr2 = np.frombuffer(buf, dtype=np.float32)
419+
assert arr2[2] == pytest.approx(3.14)
420+
421+
# const noexcept member function form
422+
cbuf = m.OneDBufferConst(4)
423+
carr = np.frombuffer(cbuf, dtype=np.float32)
424+
assert carr.shape == (4,)
425+
assert carr.flags["WRITEABLE"] is False

tests/test_methods_and_attributes.cpp

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,19 @@ class RegisteredDerived : public UnregisteredBase {
161161
double sum() const { return rw_value + ro_value; }
162162
};
163163

164-
// Issue #2234: noexcept methods in an unregistered base should be bindable on the derived class
164+
// Issue #2234: noexcept methods in an unregistered base should be bindable on the derived class.
165165
// In C++17, noexcept is part of the function type, so &Derived::method resolves to
166166
// a Base member function pointer with noexcept, requiring explicit template specializations.
167167
class NoexceptUnregisteredBase {
168168
public:
169+
// Exercises cpp_function(Return (Class::*)(Args...) const noexcept, ...)
169170
int value() const noexcept { return m_value; }
171+
// Exercises cpp_function(Return (Class::*)(Args...) noexcept, ...)
170172
void set_value(int v) noexcept { m_value = v; }
173+
// Exercises cpp_function(Return (Class::*)(Args...) & noexcept, ...)
174+
void increment() & noexcept { ++m_value; }
175+
// Exercises cpp_function(Return (Class::*)(Args...) const & noexcept, ...)
176+
int capped_value() const & noexcept { return m_value < 100 ? m_value : 100; }
171177

172178
private:
173179
int m_value = 99;
@@ -177,6 +183,17 @@ class NoexceptDerived : public NoexceptUnregisteredBase {
177183
using NoexceptUnregisteredBase::NoexceptUnregisteredBase;
178184
};
179185

186+
// Exercises overload_cast with noexcept member function pointers (issue #2234).
187+
// In C++17, overload_cast must have noexcept variants to resolve noexcept overloads.
188+
struct NoexceptOverloaded {
189+
py::str method(int) noexcept { return "(int)"; }
190+
py::str method(int) const noexcept { return "(int) const"; }
191+
py::str method(float) noexcept { return "(float)"; }
192+
};
193+
// Exercises overload_cast with noexcept free function pointers.
194+
int noexcept_free_func(int x) noexcept { return x + 1; }
195+
int noexcept_free_func(float x) noexcept { return static_cast<int>(x) + 2; }
196+
180197
// Test explicit lvalue ref-qualification
181198
struct RefQualified {
182199
int value = 0;
@@ -495,16 +512,56 @@ TEST_SUBMODULE(methods_and_attributes, m) {
495512
// unregistered base class must resolve `self` to the derived type, not the base type.
496513
py::class_<NoexceptDerived>(m, "NoexceptDerived")
497514
.def(py::init<>())
515+
// cpp_function(Return (Class::*)(Args...) const noexcept, ...)
498516
.def("value", &NoexceptDerived::value)
499-
.def("set_value", &NoexceptDerived::set_value);
517+
// cpp_function(Return (Class::*)(Args...) noexcept, ...)
518+
.def("set_value", &NoexceptDerived::set_value)
519+
// cpp_function(Return (Class::*)(Args...) & noexcept, ...)
520+
.def("increment", &NoexceptDerived::increment)
521+
// cpp_function(Return (Class::*)(Args...) const & noexcept, ...)
522+
.def("capped_value", &NoexceptDerived::capped_value);
500523

501524
#ifdef __cpp_noexcept_function_type
502-
// method_adaptor must also handle noexcept member function pointers (issue #2234)
503-
using AdaptedNoexcept = decltype(py::method_adaptor<NoexceptDerived>(&NoexceptDerived::value));
504-
static_assert(std::is_same<AdaptedNoexcept, int (NoexceptDerived::*)() const noexcept>::value,
525+
// method_adaptor must also handle noexcept member function pointers (issue #2234).
526+
// Verify the noexcept specifier is preserved in the resulting Derived pointer type.
527+
using AdaptedConstNoexcept
528+
= decltype(py::method_adaptor<NoexceptDerived>(&NoexceptDerived::value));
529+
static_assert(
530+
std::is_same<AdaptedConstNoexcept, int (NoexceptDerived::*)() const noexcept>::value, "");
531+
using AdaptedNoexcept
532+
= decltype(py::method_adaptor<NoexceptDerived>(&NoexceptDerived::set_value));
533+
static_assert(std::is_same<AdaptedNoexcept, void (NoexceptDerived::*)(int) noexcept>::value,
505534
"");
506535
#endif
507536

537+
// test_noexcept_overload_cast (issue #2234)
538+
// overload_cast must have noexcept operator() overloads so it can resolve noexcept methods.
539+
#ifdef PYBIND11_OVERLOAD_CAST
540+
py::class_<NoexceptOverloaded>(m, "NoexceptOverloaded")
541+
.def(py::init<>())
542+
// overload_cast_impl::operator()(Return (Class::*)(Args...) noexcept, false_type)
543+
.def("method", py::overload_cast<int>(&NoexceptOverloaded::method))
544+
// overload_cast_impl::operator()(Return (Class::*)(Args...) const noexcept, true_type)
545+
.def("method_const", py::overload_cast<int>(&NoexceptOverloaded::method, py::const_))
546+
// overload_cast_impl::operator()(Return (Class::*)(Args...) noexcept, false_type) float
547+
.def("method_float", py::overload_cast<float>(&NoexceptOverloaded::method));
548+
// overload_cast_impl::operator()(Return (*)(Args...) noexcept)
549+
m.def("noexcept_free_func", py::overload_cast<int>(noexcept_free_func));
550+
m.def("noexcept_free_func_float", py::overload_cast<float>(noexcept_free_func));
551+
#else
552+
// Fallback using explicit static_cast for C++11/14
553+
py::class_<NoexceptOverloaded>(m, "NoexceptOverloaded")
554+
.def(py::init<>())
555+
.def("method",
556+
static_cast<py::str (NoexceptOverloaded::*)(int)>(&NoexceptOverloaded::method))
557+
.def("method_const",
558+
static_cast<py::str (NoexceptOverloaded::*)(int) const>(&NoexceptOverloaded::method))
559+
.def("method_float",
560+
static_cast<py::str (NoexceptOverloaded::*)(float)>(&NoexceptOverloaded::method));
561+
m.def("noexcept_free_func", static_cast<int (*)(int)>(noexcept_free_func));
562+
m.def("noexcept_free_func_float", static_cast<int (*)(float)>(noexcept_free_func));
563+
#endif
564+
508565
// test_methods_and_attributes
509566
py::class_<RefQualified>(m, "RefQualified")
510567
.def(py::init<>())

tests/test_methods_and_attributes.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,11 +523,44 @@ def test_noexcept_base():
523523
In C++17 noexcept is part of the function type, so &Derived::noexcept_method resolves
524524
to a Base member-function pointer with noexcept specifier. pybind11 must use the Derived
525525
type as `self`, not the Base type, otherwise the call raises TypeError at runtime.
526+
527+
Covers all four new cpp_function constructor specialisations:
528+
- Return (Class::*)(Args...) noexcept (set_value)
529+
- Return (Class::*)(Args...) const noexcept (value)
530+
- Return (Class::*)(Args...) & noexcept (increment)
531+
- Return (Class::*)(Args...) const & noexcept (capped_value)
526532
"""
527533
obj = m.NoexceptDerived()
534+
# const noexcept
528535
assert obj.value() == 99
536+
# noexcept (non-const)
529537
obj.set_value(7)
530538
assert obj.value() == 7
539+
# & noexcept (non-const lvalue ref-qualified)
540+
obj.increment()
541+
assert obj.value() == 8
542+
# const & noexcept (const lvalue ref-qualified)
543+
assert obj.capped_value() == 8
544+
obj.set_value(200)
545+
assert obj.capped_value() == 100 # capped at 100
546+
547+
548+
def test_noexcept_overload_cast():
549+
"""Test issue #2234: overload_cast must handle noexcept member and free function pointers.
550+
551+
In C++17 noexcept is part of the function type, so overload_cast_impl needs dedicated
552+
operator() overloads for noexcept free functions and non-const/const member functions.
553+
"""
554+
obj = m.NoexceptOverloaded()
555+
# overload_cast_impl::operator()(Return (Class::*)(Args...) noexcept, false_type)
556+
assert obj.method(1) == "(int)"
557+
# overload_cast_impl::operator()(Return (Class::*)(Args...) const noexcept, true_type)
558+
assert obj.method_const(2) == "(int) const"
559+
# overload_cast_impl::operator()(Return (Class::*)(Args...) noexcept, false_type) float
560+
assert obj.method_float(3.0) == "(float)"
561+
# overload_cast_impl::operator()(Return (*)(Args...) noexcept)
562+
assert m.noexcept_free_func(10) == 11
563+
assert m.noexcept_free_func_float(10.0) == 12
531564

532565

533566
def test_ref_qualified():

tests/test_numpy_vectorize.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,21 @@ TEST_SUBMODULE(numpy_vectorize, m) {
7878
struct VectorizeTestClass {
7979
explicit VectorizeTestClass(int v) : value{v} {};
8080
float method(int x, float y) const { return y + (float) (x + value); }
81+
// Exercises vectorize(Return (Class::*)(Args...) noexcept)
82+
float method_noexcept(int x, float y) noexcept { return y + (float) (x + value); }
83+
// Exercises vectorize(Return (Class::*)(Args...) const noexcept)
84+
float method_const_noexcept(int x, float y) const noexcept {
85+
return y + (float) (x + value);
86+
}
8187
int value = 0;
8288
};
8389
py::class_<VectorizeTestClass> vtc(m, "VectorizeTestClass");
8490
vtc.def(py::init<int>()).def_readwrite("value", &VectorizeTestClass::value);
8591

8692
// Automatic vectorizing of methods
8793
vtc.def("method", py::vectorize(&VectorizeTestClass::method));
94+
vtc.def("method_noexcept", py::vectorize(&VectorizeTestClass::method_noexcept));
95+
vtc.def("method_const_noexcept", py::vectorize(&VectorizeTestClass::method_const_noexcept));
8896

8997
// test_trivial_broadcasting
9098
// Internal optimization test for whether the input is trivially broadcastable:

tests/test_numpy_vectorize.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,22 @@ def test_method_vectorization():
246246
assert np.all(o.method(x, y) == [[14, 15], [24, 25]])
247247

248248

249+
def test_noexcept_method_vectorization():
250+
"""Test issue #2234: vectorize must handle noexcept member function pointers.
251+
252+
Covers both new vectorize specialisations:
253+
- vectorize(Return (Class::*)(Args...) noexcept)
254+
- vectorize(Return (Class::*)(Args...) const noexcept)
255+
"""
256+
o = m.VectorizeTestClass(3)
257+
x = np.array([1, 2], dtype="int")
258+
y = np.array([[10], [20]], dtype="float32")
259+
# vectorize(Return (Class::*)(Args...) noexcept)
260+
assert np.all(o.method_noexcept(x, y) == [[14, 15], [24, 25]])
261+
# vectorize(Return (Class::*)(Args...) const noexcept)
262+
assert np.all(o.method_const_noexcept(x, y) == [[14, 15], [24, 25]])
263+
264+
249265
def test_array_collapse():
250266
assert not isinstance(m.vectorized_func(1, 2, 3), np.ndarray)
251267
assert not isinstance(m.vectorized_func(np.array(1), 2, 3), np.ndarray)

0 commit comments

Comments
 (0)