From f34a0392914d6a3ca1579e2437c1e24fb2525adb Mon Sep 17 00:00:00 2001 From: Scott Wolchok Date: Fri, 31 Oct 2025 12:49:44 -0700 Subject: [PATCH 01/11] Improve performance of enum_ operators by going back to specific implementation test_enum needs a patch because ops are now overloaded and this affects their docstrings. --- include/pybind11/detail/common.h | 8 ++++ include/pybind11/pybind11.h | 80 +++++++++++++++++++++----------- tests/test_enum.py | 13 +++++- 3 files changed, 74 insertions(+), 27 deletions(-) diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 16952c5829..75a7125314 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -163,6 +163,14 @@ # define PYBIND11_NOINLINE __attribute__((noinline)) inline #endif +#if defined(_MSC_VER) +# define PYBIND11_ALWAYS_INLINE __forceinline +#elif defined(__GNUC__) +# define PYBIND11_ALWAYS_INLINE __attribute__((__always_inline__)) inline +#else +# define PYBIND11_ALWAYS_INLINE inline +#endif + #if defined(__MINGW32__) // For unknown reasons all PYBIND11_DEPRECATED member trigger a warning when declared // whether it is used or not diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 8ab4681c76..9f31b471d7 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2218,7 +2218,7 @@ class class_ : public detail::generic_type { static void add_base(detail::type_record &) {} template - class_ &def(const char *name_, Func &&f, const Extra &...extra) { + PYBIND11_ALWAYS_INLINE class_ &def(const char *name_, Func &&f, const Extra &...extra) { cpp_function cf(method_adaptor(std::forward(f)), name(name_), is_method(*this), @@ -2797,38 +2797,13 @@ struct enum_base { pos_only()) if (is_convertible) { - PYBIND11_ENUM_OP_CONV_LHS("__eq__", !b.is_none() && a.equal(b)); - PYBIND11_ENUM_OP_CONV_LHS("__ne__", b.is_none() || !a.equal(b)); - if (is_arithmetic) { - PYBIND11_ENUM_OP_CONV("__lt__", a < b); - PYBIND11_ENUM_OP_CONV("__gt__", a > b); - PYBIND11_ENUM_OP_CONV("__le__", a <= b); - PYBIND11_ENUM_OP_CONV("__ge__", a >= b); - PYBIND11_ENUM_OP_CONV("__and__", a & b); - PYBIND11_ENUM_OP_CONV("__rand__", a & b); - PYBIND11_ENUM_OP_CONV("__or__", a | b); - PYBIND11_ENUM_OP_CONV("__ror__", a | b); - PYBIND11_ENUM_OP_CONV("__xor__", a ^ b); - PYBIND11_ENUM_OP_CONV("__rxor__", a ^ b); m_base.attr("__invert__") = cpp_function([](const object &arg) { return ~(int_(arg)); }, name("__invert__"), is_method(m_base), pos_only()); } - } else { - PYBIND11_ENUM_OP_STRICT("__eq__", int_(a).equal(int_(b)), return false); - PYBIND11_ENUM_OP_STRICT("__ne__", !int_(a).equal(int_(b)), return true); - - if (is_arithmetic) { -#define PYBIND11_THROW throw type_error("Expected an enumeration of matching type!"); - PYBIND11_ENUM_OP_STRICT("__lt__", int_(a) < int_(b), PYBIND11_THROW); - PYBIND11_ENUM_OP_STRICT("__gt__", int_(a) > int_(b), PYBIND11_THROW); - PYBIND11_ENUM_OP_STRICT("__le__", int_(a) <= int_(b), PYBIND11_THROW); - PYBIND11_ENUM_OP_STRICT("__ge__", int_(a) >= int_(b), PYBIND11_THROW); -#undef PYBIND11_THROW - } } #undef PYBIND11_ENUM_OP_CONV_LHS @@ -2944,6 +2919,59 @@ class enum_ : public class_ { def(init([](Scalar i) { return static_cast(i); }), arg("value")); def_property_readonly("value", [](Type value) { return (Scalar) value; }, pos_only()); +#define PYBIND11_ENUM_OP_SAME_TYPE(op, expr) \ + def(op, [](Type a, Type b) { return expr; }, pybind11::name(op), arg("other"), pos_only()) +#define PYBIND11_ENUM_OP_SAME_TYPE_RHS_MAY_BE_NONE(op, expr) \ + def(op, [](Type a, Type *b_ptr) { return expr; }, pybind11::name(op), arg("other"), pos_only()) +#define PYBIND11_ENUM_OP_SCALAR(op, op_expr) \ + def( \ + op, \ + [](Type a, Scalar b) { return static_cast(a) op_expr b; }, \ + pybind11::name(op), \ + arg("other"), \ + pos_only()) +#define PYBIND11_ENUM_OP_CONV_ARITHMETIC(op, op_expr) \ + PYBIND11_ENUM_OP_SAME_TYPE(op, static_cast(a) op_expr static_cast(b)); \ + PYBIND11_ENUM_OP_SCALAR(op, op_expr) +#define PYBIND11_ENUM_OP_REJECT_UNRELATED_TYPE(op, strict_behavior) \ + def( \ + op, \ + [](Type, const object &) { strict_behavior; }, \ + pybind11::name(op), \ + arg("other"), \ + pos_only()) +#define PYBIND11_ENUM_OP_STRICT_ARITHMETIC(op, op_expr, strict_behavior) \ + PYBIND11_ENUM_OP_SAME_TYPE(op, static_cast(a) op_expr static_cast(b)); \ + PYBIND11_ENUM_OP_REJECT_UNRELATED_TYPE(op, strict_behavior); + + PYBIND11_ENUM_OP_SAME_TYPE_RHS_MAY_BE_NONE("__eq__", b_ptr && a == *b_ptr); + PYBIND11_ENUM_OP_SAME_TYPE_RHS_MAY_BE_NONE("__ne__", !b_ptr || a != *b_ptr); + if (std::is_convertible::value) { + PYBIND11_ENUM_OP_SCALAR("__eq__", ==); + PYBIND11_ENUM_OP_SCALAR("__ne__", !=); + if (is_arithmetic) { + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__lt__", <); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__gt__", >); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__le__", <=); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__ge__", >=); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__and__", &); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__rand__", &); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__or__", |); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__ror__", |); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__xor__", ^); + PYBIND11_ENUM_OP_CONV_ARITHMETIC("__rxor__", ^); + } + } else if (is_arithmetic) { +#define PYBIND11_THROW throw type_error("Expected an enumeration of matching type!"); + PYBIND11_ENUM_OP_STRICT_ARITHMETIC("__lt__", <, PYBIND11_THROW); + PYBIND11_ENUM_OP_STRICT_ARITHMETIC("__gt__", >, PYBIND11_THROW); + PYBIND11_ENUM_OP_STRICT_ARITHMETIC("__le__", <=, PYBIND11_THROW); + PYBIND11_ENUM_OP_STRICT_ARITHMETIC("__ge__", >=, PYBIND11_THROW); +#undef PYBIND11_THROW + } + PYBIND11_ENUM_OP_REJECT_UNRELATED_TYPE("__eq__", return false); + PYBIND11_ENUM_OP_REJECT_UNRELATED_TYPE("__ne__", return true); + def("__int__", [](Type value) { return (Scalar) value; }, pos_only()); def("__index__", [](Type value) { return (Scalar) value; }, pos_only()); attr("__setstate__") = cpp_function( diff --git a/tests/test_enum.py b/tests/test_enum.py index 99d4a88c8a..9fc00cad6b 100644 --- a/tests/test_enum.py +++ b/tests/test_enum.py @@ -295,10 +295,21 @@ def test_generated_dunder_methods_pos_only(): "__rxor__", ]: method = getattr(enum_type, binary_op, None) + # TODO: docs now show overloading; update if method is not None: + # 1) The docs must start with the name of the op. assert ( re.match( - rf"^{binary_op}\(self: [\w\.]+, other: [\w\.]+, /\)", + rf"^{binary_op}\(", + method.__doc__, + ) + is not None + ) + # 2) The docs must contain the op's signature. This is a separate check + # and not anchored at the start because the op may be overloaded. + assert ( + re.search( + rf"{binary_op}\(self: [\w\.]+, other: [\w\.]+, /\)", method.__doc__, ) is not None From a65e79d8ca5b2df91bdef6e5667d3bda7b258fad Mon Sep 17 00:00:00 2001 From: Scott Wolchok Date: Tue, 7 Oct 2025 17:54:05 -0700 Subject: [PATCH 02/11] outline call_impl to save on code size This does cause more move constructions, as shown by the needed update to test_copy_move. Up to reviewers whether they want more code size or more moves. --- include/pybind11/detail/function_ref.h | 97 ++++++++++++++++++++++++++ include/pybind11/pybind11.h | 65 ++++++++++------- tests/test_copy_move.py | 6 +- 3 files changed, 140 insertions(+), 28 deletions(-) create mode 100644 include/pybind11/detail/function_ref.h diff --git a/include/pybind11/detail/function_ref.h b/include/pybind11/detail/function_ref.h new file mode 100644 index 0000000000..7b8deecf5b --- /dev/null +++ b/include/pybind11/detail/function_ref.h @@ -0,0 +1,97 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +//===- llvm/ADT/STLFunctionalExtras.h - Extras for -*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This file contains some extension to . +// +// No library is required when using these functions. +// +//===----------------------------------------------------------------------===// +// Extra additions to +//===----------------------------------------------------------------------===// + +/// An efficient, type-erasing, non-owning reference to a callable. This is +/// intended for use as the type of a function parameter that is not used +/// after the function in question returns. +/// +/// This class does not own the callable, so it is not in general safe to store +/// a FunctionRef. + +// pybind11: modified again from executorch::runtime::FunctionRef +// - renamed back to function_ref +// - use pybind11 enable_if_t, remove_cvref_t, and remove_reference_t + +// torch::executor: modified from llvm::function_ref +// - renamed to FunctionRef +// - removed LLVM_GSL_POINTER and LLVM_LIFETIME_BOUND macro uses +// - use namespaced internal::remove_cvref_t + +#pragma once + +#include + +#include +#include +#include + +PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) +PYBIND11_NAMESPACE_BEGIN(detail) + +//===----------------------------------------------------------------------===// +// Features from C++20 +//===----------------------------------------------------------------------===// + +template +class function_ref; + +template +class function_ref { + Ret (*callback)(intptr_t callable, Params... params) = nullptr; + intptr_t callable; + + template + static Ret callback_fn(intptr_t callable, Params... params) { + return (*reinterpret_cast(callable))(std::forward(params)...); + } + +public: + function_ref() = default; + function_ref(std::nullptr_t) {} + + template + function_ref( + Callable &&callable, + // This is not the copy-constructor. + enable_if_t, function_ref>::value> * = nullptr, + // Functor must be callable and return a suitable type. + enable_if_t< + std::is_void::value + || std::is_convertible()(std::declval()...)), + Ret>::value> * = nullptr) + : callback(callback_fn>), + callable(reinterpret_cast(&callable)) {} + + Ret operator()(Params... params) const { + return callback(callable, std::forward(params)...); + } + + explicit operator bool() const { return callback; } + + bool operator==(const function_ref &Other) const { + return callable == Other.callable; + } +}; +PYBIND11_NAMESPACE_END(detail) +PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 9f31b471d7..6c60dc206f 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -13,6 +13,7 @@ #include "detail/dynamic_raw_ptr_cast_if_possible.h" #include "detail/exception_translation.h" #include "detail/function_record_pyobject.h" +#include "detail/function_ref.h" #include "detail/init.h" #include "detail/native_enum_data.h" #include "detail/using_smart_holder.h" @@ -379,6 +380,40 @@ class cpp_function : public function { return unique_function_record(new detail::function_record()); } +private: + // This is outlined from the dispatch lambda in initialize to save + // on code size. Crucially, we use function_ref to type-erase the + // actual function lambda so that we can get code reuse for + // functions with the same Return, Args, and Guard. + template + static handle call_impl(detail::function_call &call, detail::function_ref f) { + using namespace detail; + using cast_out + = make_caster::value, void_type, Return>>; + + ArgsConverter args_converter; + if (!args_converter.load_args(call)) { + return PYBIND11_TRY_NEXT_OVERLOAD; + } + + /* Override policy for rvalues -- usually to enforce rvp::move on an rvalue */ + return_value_policy policy + = return_value_policy_override::policy(call.func.policy); + + /* Perform the function call */ + handle result; + if (call.func.is_setter) { + (void) std::move(args_converter).template call(f); + result = none().release(); + } else { + result = cast_out::cast( + std::move(args_converter).template call(f), policy, call.parent); + } + + return result; + } + +protected: /// Special internal constructor for functors, lambda functions, etc. template void initialize(Func &&f, Return (*)(Args...), const Extra &...extra) { @@ -441,13 +476,6 @@ class cpp_function : public function { /* Dispatch code which converts function arguments and performs the actual function call */ rec->impl = [](function_call &call) -> handle { - cast_in args_converter; - - /* Try to cast the function arguments into the C++ domain */ - if (!args_converter.load_args(call)) { - return PYBIND11_TRY_NEXT_OVERLOAD; - } - /* Invoke call policy pre-call hook */ process_attributes::precall(call); @@ -456,24 +484,11 @@ class cpp_function : public function { : call.func.data[0]); auto *cap = const_cast(reinterpret_cast(data)); - /* Override policy for rvalues -- usually to enforce rvp::move on an rvalue */ - return_value_policy policy - = return_value_policy_override::policy(call.func.policy); - - /* Function scope guard -- defaults to the compile-to-nothing `void_type` */ - using Guard = extract_guard_t; - - /* Perform the function call */ - handle result; - if (call.func.is_setter) { - (void) std::move(args_converter).template call(cap->f); - result = none().release(); - } else { - result = cast_out::cast( - std::move(args_converter).template call(cap->f), - policy, - call.parent); - } + auto result = call_impl, + cast_in>(call, detail::function_ref(cap->f)); /* Invoke call policy post-call hook */ process_attributes::postcall(call, result); diff --git a/tests/test_copy_move.py b/tests/test_copy_move.py index 3a3f293414..1489f43974 100644 --- a/tests/test_copy_move.py +++ b/tests/test_copy_move.py @@ -70,12 +70,12 @@ def test_move_and_copy_loads(): assert c_m.copy_assignments + c_m.copy_constructions == 0 assert c_m.move_assignments == 6 - assert c_m.move_constructions == 9 + assert c_m.move_constructions == 21 assert c_mc.copy_assignments + c_mc.copy_constructions == 0 assert c_mc.move_assignments == 5 - assert c_mc.move_constructions == 8 + assert c_mc.move_constructions == 18 assert c_c.copy_assignments == 4 - assert c_c.copy_constructions == 6 + assert c_c.copy_constructions == 14 assert c_m.alive() + c_mc.alive() + c_c.alive() == 0 From ffb981c376b9beaa5d4a73abe9db614e6ce70ac2 Mon Sep 17 00:00:00 2001 From: Scott Wolchok Date: Fri, 31 Oct 2025 14:32:54 -0700 Subject: [PATCH 03/11] add function_ref.h to PYBIND11_HEADERS. --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8063303938..3edd13cbbe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -188,6 +188,7 @@ set(PYBIND11_HEADERS include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h include/pybind11/detail/exception_translation.h include/pybind11/detail/function_record_pyobject.h + include/pybind11/detail/function_ref.h include/pybind11/detail/holder_caster_foreign_helpers.h include/pybind11/detail/init.h include/pybind11/detail/internals.h From 65e586601f4c939a697351eaefd139235928f02d Mon Sep 17 00:00:00 2001 From: Scott Wolchok Date: Mon, 3 Nov 2025 15:16:15 -0800 Subject: [PATCH 04/11] Update test_copy_move tests with C++17 passing values just so we can see mostly-not-red tests --- tests/test_copy_move.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_copy_move.py b/tests/test_copy_move.py index 1489f43974..d843793350 100644 --- a/tests/test_copy_move.py +++ b/tests/test_copy_move.py @@ -103,12 +103,12 @@ def test_move_and_copy_load_optional(): assert c_m.copy_assignments + c_m.copy_constructions == 0 assert c_m.move_assignments == 2 - assert c_m.move_constructions == 5 + assert c_m.move_constructions == 9 assert c_mc.copy_assignments + c_mc.copy_constructions == 0 assert c_mc.move_assignments == 2 - assert c_mc.move_constructions == 5 + assert c_mc.move_constructions == 9 assert c_c.copy_assignments == 2 - assert c_c.copy_constructions == 5 + assert c_c.copy_constructions == 9 assert c_m.alive() + c_mc.alive() + c_c.alive() == 0 From 729e9f8d358100ba7497d0e8fc19a136f670cb22 Mon Sep 17 00:00:00 2001 From: Scott Wolchok Date: Mon, 3 Nov 2025 15:17:12 -0800 Subject: [PATCH 05/11] Remove stray TODO --- tests/test_enum.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_enum.py b/tests/test_enum.py index 9fc00cad6b..160708ef53 100644 --- a/tests/test_enum.py +++ b/tests/test_enum.py @@ -295,7 +295,6 @@ def test_generated_dunder_methods_pos_only(): "__rxor__", ]: method = getattr(enum_type, binary_op, None) - # TODO: docs now show overloading; update if method is not None: # 1) The docs must start with the name of the op. assert ( From f24ded5e4d373cde772c90b55c8829e7511d6912 Mon Sep 17 00:00:00 2001 From: Scott Wolchok Date: Wed, 5 Nov 2025 09:32:58 -0800 Subject: [PATCH 06/11] fix clang-tidy --- include/pybind11/detail/function_ref.h | 4 ++++ include/pybind11/pybind11.h | 2 ++ 2 files changed, 6 insertions(+) diff --git a/include/pybind11/detail/function_ref.h b/include/pybind11/detail/function_ref.h index 7b8deecf5b..b865271489 100644 --- a/include/pybind11/detail/function_ref.h +++ b/include/pybind11/detail/function_ref.h @@ -32,6 +32,7 @@ // pybind11: modified again from executorch::runtime::FunctionRef // - renamed back to function_ref // - use pybind11 enable_if_t, remove_cvref_t, and remove_reference_t +// - lint suppressions // torch::executor: modified from llvm::function_ref // - renamed to FunctionRef @@ -63,14 +64,17 @@ class function_ref { template static Ret callback_fn(intptr_t callable, Params... params) { + // NOLINTNEXTLINE(performance-no-int-to-ptr) return (*reinterpret_cast(callable))(std::forward(params)...); } public: function_ref() = default; + // NOLINTNEXTLINE(google-explicit-constructor) function_ref(std::nullptr_t) {} template + // NOLINTNEXTLINE(google-explicit-constructor) function_ref( Callable &&callable, // This is not the copy-constructor. diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 6c60dc206f..7214d482d5 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2946,6 +2946,7 @@ class enum_ : public class_ { arg("other"), \ pos_only()) #define PYBIND11_ENUM_OP_CONV_ARITHMETIC(op, op_expr) \ + /* NOLINTNEXTLINE(bugprone-macro-parentheses) */ \ PYBIND11_ENUM_OP_SAME_TYPE(op, static_cast(a) op_expr static_cast(b)); \ PYBIND11_ENUM_OP_SCALAR(op, op_expr) #define PYBIND11_ENUM_OP_REJECT_UNRELATED_TYPE(op, strict_behavior) \ @@ -2956,6 +2957,7 @@ class enum_ : public class_ { arg("other"), \ pos_only()) #define PYBIND11_ENUM_OP_STRICT_ARITHMETIC(op, op_expr, strict_behavior) \ + /* NOLINTNEXTLINE(bugprone-macro-parentheses) */ \ PYBIND11_ENUM_OP_SAME_TYPE(op, static_cast(a) op_expr static_cast(b)); \ PYBIND11_ENUM_OP_REJECT_UNRELATED_TYPE(op, strict_behavior); From c032b533a3c8c6217bf59c588764c11677d6522d Mon Sep 17 00:00:00 2001 From: Scott Wolchok Date: Thu, 6 Nov 2025 10:23:32 -0800 Subject: [PATCH 07/11] fix clang-tidy again. add function_ref.h to test_files.py --- include/pybind11/detail/function_ref.h | 2 ++ tests/extra_python_package/test_files.py | 1 + 2 files changed, 3 insertions(+) diff --git a/include/pybind11/detail/function_ref.h b/include/pybind11/detail/function_ref.h index b865271489..f99724cbfc 100644 --- a/include/pybind11/detail/function_ref.h +++ b/include/pybind11/detail/function_ref.h @@ -63,6 +63,7 @@ class function_ref { intptr_t callable; template + // NOLINTNEXTLINE(performance-unnecessary-value-param) static Ret callback_fn(intptr_t callable, Params... params) { // NOLINTNEXTLINE(performance-no-int-to-ptr) return (*reinterpret_cast(callable))(std::forward(params)...); @@ -87,6 +88,7 @@ class function_ref { : callback(callback_fn>), callable(reinterpret_cast(&callable)) {} + // NOLINTNEXTLINE(performance-unnecessary-value-param) Ret operator()(Params... params) const { return callback(callable, std::forward(params)...); } diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py index 1539b171a2..d96e9afc1f 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -83,6 +83,7 @@ "include/pybind11/detail/descr.h", "include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h", "include/pybind11/detail/function_record_pyobject.h", + "include/pybind11/detail/function_ref.h", "include/pybind11/detail/holder_caster_foreign_helpers.h", "include/pybind11/detail/init.h", "include/pybind11/detail/internals.h", From 279c72a04f6cb3062ee862713005c4f37d4d47f3 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 11 Nov 2025 11:07:21 -0800 Subject: [PATCH 08/11] Add static assertion for function_ref lifetime safety in call_impl Add a static_assert to document and enforce that function_ref is trivially copyable, ensuring safe pass-by-value usage. This also documents the lifetime safety guarantees: function_ref is created from cap->f which lives in the capture object, and is only used synchronously within call_impl without being stored beyond its scope. --- include/pybind11/pybind11.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 7214d482d5..ee1b357526 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -388,6 +388,12 @@ class cpp_function : public function { template static handle call_impl(detail::function_call &call, detail::function_ref f) { using namespace detail; + // Static assertion: function_ref must be trivially copyable to ensure safe pass-by-value. + // Lifetime safety: The function_ref is created from cap->f which lives in the capture + // object stored in the function record, and is only used synchronously within this + // function call. It is never stored beyond the scope of call_impl. + static_assert(std::is_trivially_copyable>::value, + "function_ref must be trivially copyable for safe pass-by-value usage"); using cast_out = make_caster::value, void_type, Return>>; From a580ccebf390661f3fb725192827996d19c51dbb Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 11 Nov 2025 11:14:45 -0800 Subject: [PATCH 09/11] Add #undef cleanup for enum operator macros Undefine all enum operator macros after their last use to prevent macro pollution and follow the existing code pattern. This matches the cleanup pattern used for the previous enum operator macros. --- include/pybind11/pybind11.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index ee1b357526..3bec3b16f5 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2995,6 +2995,13 @@ class enum_ : public class_ { PYBIND11_ENUM_OP_REJECT_UNRELATED_TYPE("__eq__", return false); PYBIND11_ENUM_OP_REJECT_UNRELATED_TYPE("__ne__", return true); +#undef PYBIND11_ENUM_OP_SAME_TYPE +#undef PYBIND11_ENUM_OP_SAME_TYPE_RHS_MAY_BE_NONE +#undef PYBIND11_ENUM_OP_SCALAR +#undef PYBIND11_ENUM_OP_CONV_ARITHMETIC +#undef PYBIND11_ENUM_OP_REJECT_UNRELATED_TYPE +#undef PYBIND11_ENUM_OP_STRICT_ARITHMETIC + def("__int__", [](Type value) { return (Scalar) value; }, pos_only()); def("__index__", [](Type value) { return (Scalar) value; }, pos_only()); attr("__setstate__") = cpp_function( From 0287ec6858f002143752711a9876154b7202fb7d Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 11 Nov 2025 11:20:34 -0800 Subject: [PATCH 10/11] Rename PYBIND11_THROW to PYBIND11_ENUM_OP_THROW_TYPE_ERROR Rename the macro to be more specific and avoid potential clashes with public macros. The new name clearly indicates it's scoped to enum operations and describes its purpose (throwing a type error). --- include/pybind11/pybind11.h | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 3bec3b16f5..6f281c46e8 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2985,12 +2985,13 @@ class enum_ : public class_ { PYBIND11_ENUM_OP_CONV_ARITHMETIC("__rxor__", ^); } } else if (is_arithmetic) { -#define PYBIND11_THROW throw type_error("Expected an enumeration of matching type!"); - PYBIND11_ENUM_OP_STRICT_ARITHMETIC("__lt__", <, PYBIND11_THROW); - PYBIND11_ENUM_OP_STRICT_ARITHMETIC("__gt__", >, PYBIND11_THROW); - PYBIND11_ENUM_OP_STRICT_ARITHMETIC("__le__", <=, PYBIND11_THROW); - PYBIND11_ENUM_OP_STRICT_ARITHMETIC("__ge__", >=, PYBIND11_THROW); -#undef PYBIND11_THROW +#define PYBIND11_ENUM_OP_THROW_TYPE_ERROR \ + throw type_error("Expected an enumeration of matching type!"); + PYBIND11_ENUM_OP_STRICT_ARITHMETIC("__lt__", <, PYBIND11_ENUM_OP_THROW_TYPE_ERROR); + PYBIND11_ENUM_OP_STRICT_ARITHMETIC("__gt__", >, PYBIND11_ENUM_OP_THROW_TYPE_ERROR); + PYBIND11_ENUM_OP_STRICT_ARITHMETIC("__le__", <=, PYBIND11_ENUM_OP_THROW_TYPE_ERROR); + PYBIND11_ENUM_OP_STRICT_ARITHMETIC("__ge__", >=, PYBIND11_ENUM_OP_THROW_TYPE_ERROR); +#undef PYBIND11_ENUM_OP_THROW_TYPE_ERROR } PYBIND11_ENUM_OP_REJECT_UNRELATED_TYPE("__eq__", return false); PYBIND11_ENUM_OP_REJECT_UNRELATED_TYPE("__ne__", return true); From 35b4b8f26d952602f544e1c34139629186046208 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 11 Nov 2025 11:25:13 -0800 Subject: [PATCH 11/11] Clarify comments in function_ref.h Replace vague comments about 'extensions to ' and 'functions' with a clearer description that this is a header-only class template similar to std::function but with non-owning semantics. This makes it clear that it's template-only and requires no additional library linking. --- include/pybind11/detail/function_ref.h | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/include/pybind11/detail/function_ref.h b/include/pybind11/detail/function_ref.h index f99724cbfc..a81bdfe13f 100644 --- a/include/pybind11/detail/function_ref.h +++ b/include/pybind11/detail/function_ref.h @@ -14,12 +14,10 @@ // //===----------------------------------------------------------------------===// // -// This file contains some extension to . +// This file contains a header-only class template that provides functionality +// similar to std::function but with non-owning semantics. It is a template-only +// implementation that requires no additional library linking. // -// No library is required when using these functions. -// -//===----------------------------------------------------------------------===// -// Extra additions to //===----------------------------------------------------------------------===// /// An efficient, type-erasing, non-owning reference to a callable. This is