Skip to content

Comments

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

Open
Skylion007 wants to merge 16 commits intopybind:masterfrom
Skylion007:skylion007/cpp17-claude-noexcept-2026-02-20
Open

fix: bind noexcept and ref-qualified methods from unregistered base classes#5992
Skylion007 wants to merge 16 commits intopybind:masterfrom
Skylion007:skylion007/cpp17-claude-noexcept-2026-02-20

Conversation

@Skylion007
Copy link
Collaborator

@Skylion007 Skylion007 commented Feb 20, 2026

Description

Fix issue #2234 . Fix overloading to accept noexcept function types too and strip them. Mostly authored by ClaudeCode with a lot of human intervention.

fix: support noexcept and ref-qualified methods from unregistered base classes (issue #2234)

Problem

In C++17, noexcept became part of the function type (P0012R1). This means
int (Base::*)() const noexcept and int (Base::*)() const are distinct
types
. When a derived class inherits methods from an unregistered base,
method_adaptor must recast the member function pointer from Base::* to
Derived::*. Without explicit template specializations for noexcept (and
ref-qualified) variants, the generic F&& fallback was selected, leaving the
pointer typed as Base::* — causing pybind11 to look up Base as self,
which fails at runtime for unregistered base classes.

Similarly, cpp_function lacked constructors for noexcept, & noexcept,
const noexcept, const & noexcept, &&, const &&, && noexcept, and
const && noexcept member function pointer types, so those overloads silently
fell through to the generic lambda constructor.

Solution

include/pybind11/pybind11.h

  • Add cpp_function constructors for all missing qualifier combinations:
    • &&, const && (unconditional): use std::move(*c).*f in the lambda
    • noexcept, const noexcept, & noexcept, const & noexcept,
      && noexcept, const && noexcept (under #ifdef __cpp_noexcept_function_type)
  • Add detail::rebind_member_ptr<Derived, T> type trait mapping
    Return (Base::*)(Args...) qualifiers to Return (Derived::*)(Args...) qualifiers
    across all 12 qualifier combinations, generated via a local
    PYBIND11_REBIND_MEMBER_PTR macro.
  • Add detail::adapt_member_ptr<Derived>(pmf) — shared constexpr helper
    called by all method_adaptor overloads: runs the is_accessible_base_of
    static_assert then returns the implicitly-cast pointer.
  • Replace the ad-hoc method_adaptor overloads with a PYBIND11_METHOD_ADAPTOR
    macro covering all 11 qualified variants (plus the no-qualifier overload
    written explicitly to avoid MSVC C4003). All overloads are constexpr.
  • Add unconditional & / const & method_adaptor overloads (no #ifdef
    guard) to fix C++14 builds where noexcept is stripped from the type but
    the & qualifier is not.

include/pybind11/numpy.h

  • Add vectorize overloads for noexcept free function pointers.

Tests

  • test_methods_and_attributes: NoexceptDerived / NoexceptUnregisteredBase
    (C++17 noexcept from unregistered base), RValueRefDerived /
    RValueRefUnregisteredBase (&&/const && methods that move-out state to
    confirm std::move(*c).*f semantics), NoexceptOverloaded (overload_cast
    with noexcept), static_asserts verifying method_adaptor preserves all
    qualifier combinations.
  • test_buffers: def_buffer with noexcept member function pointers.
  • test_numpy_vectorize: vectorize with noexcept free function pointers.

Compatibility

  • No behaviour change for existing code — all new cpp_function constructors
    and method_adaptor overloads are additive and selected only for the new
    type signatures.
  • C++14 compatible (noexcept constructors/overloads gated on
    __cpp_noexcept_function_type).
  • MSVC C4003 avoided by writing the no-qualifier specializations explicitly
    rather than invoking macros with an empty argument.
  • clang-tidy bugprone-macro-parentheses suppressed with NOLINTBEGIN /
    NOLINTEND around the macro definitions (the qualifiers argument appears
    in type position where parenthesizing it would be invalid C++).

Suggested changelog entry:

  • Fix binding of noexcept and ref-qualified (&, &&) methods
    inherited from unregistered base classes in C++17, where noexcept is
    part of the function type. method_adaptor now has specializations for
    all qualifier combinations (const, &, const &, &&,
    const &&, and their noexcept variants), and cpp_function gains
    matching constructors. &&-qualified methods correctly use
    std::move(*self).*f in the generated lambda.

@Skylion007 Skylion007 requested a review from rwgk February 20, 2026 18:49
@Skylion007 Skylion007 force-pushed the skylion007/cpp17-claude-noexcept-2026-02-20 branch from a7a3ed9 to e8b0584 Compare February 20, 2026 19:36
@Skylion007 Skylion007 force-pushed the skylion007/cpp17-claude-noexcept-2026-02-20 branch from e8b0584 to b303194 Compare February 20, 2026 19:48
@Skylion007 Skylion007 force-pushed the skylion007/cpp17-claude-noexcept-2026-02-20 branch from ad30bc0 to f530adb Compare February 20, 2026 20:26
@Skylion007 Skylion007 changed the title Strip noexcept from cpp17 function type bindings Improve coverage of most method types Feb 21, 2026
@Skylion007 Skylion007 marked this pull request as ready for review February 21, 2026 20:30
@Skylion007 Skylion007 changed the title Improve coverage of most method types fix: bind noexcept and ref-qualified methods from unregistered base classes Feb 21, 2026
// (which match plain Return(Args...)) work correctly with noexcept callables (issue #2234).
template <typename Function, typename F = remove_reference_t<Function>>
using function_signature_t = conditional_t<
using function_signature_t = remove_noexcept_t<conditional_t<
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I normalize it here for all pybind11? Or keep it as utility that and create a new alias that always strips the noexcept

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on the flip side we could use the noexcept tag handling to optimize away exception handling for certain edge cases like def_buffer maybe?


// Exercises cpp_function(Return (Class::*)(Args...) &&, ...) and
// cpp_function(Return (Class::*)(Args...) const &&, ...) via an unregistered base.
class RValueRefUnregisteredBase {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also unsure if should encourage rvalue overloads, but they were always supported by manually calling them via lambdas sooo

@rwgk
Copy link
Collaborator

rwgk commented Feb 23, 2026

Hi @Skylion007 could you please validate my high-level understanding of this PR?

This is the result of me chatting with Cursor (GPT-5.3 Codex High) for a bit:


  • Status quo on current master is inconsistent/partial.

    • Some noexcept callables bind today (often simpler signatures).
    • Others fail to bind cleanly, or fall through less-specific template paths.
    • In some cases (notably around unregistered-base/member-pointer adaptation), binding may compile but not behave correctly at runtime.
  • Throwing through bound noexcept functions remains process-terminating by C++ semantics.

    • If a C++ exception escapes a noexcept function, std::terminate is triggered.
    • This preempts pybind11 exception translation (error_already_set -> Python exception).
    • Therefore, Python-visible calls may abort the process if a bound noexcept function throws.
  • What PR 5992 improves.

    • It broadens/normalizes qualifier handling (const, &, &&, and noexcept combinations), making signature matching more uniform and predictable.
    • It reduces accidental fallback paths and fixes specific C++17 noexcept-typed member-pointer adaptation issues (issue noexcept base class methods are not recognized in C++17 mode #2234 context).
    • It does not change the underlying C++ runtime rule for exceptions escaping noexcept.

rwgk and others added 5 commits February 22, 2026 17:25
A production-code review after pybind#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>
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>
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>
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>
…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>
@rwgk
Copy link
Collaborator

rwgk commented Feb 23, 2026

Hi @Skylion007 I added five Cursor-generated commits (all using GPT-5.3 Codex High). I'll do a manual review here after I see that the CI is still passing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants