Skip to content

pybind11: bind member functions via a forwarding lambda (fixes MSVC C2440 on overloaded auto-returning members of class templates)#38

Open
Fedr wants to merge 4 commits into
masterfrom
msvc-overloaded-auto-return-method
Open

pybind11: bind member functions via a forwarding lambda (fixes MSVC C2440 on overloaded auto-returning members of class templates)#38
Fedr wants to merge 4 commits into
masterfrom
msvc-overloaded-auto-return-method

Conversation

@Fedr

@Fedr Fedr commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Problem

Member functions are bound by disambiguating overloads with a pointer-to-member cast:

static_cast<Ret (C::*)(Args) const>(&C::name)

MSVC (through 19.5x / VS 2026) rejects that cast when the member is overloaded, has a deduced (auto) return type, and the enclosing class is a template — all three are required:

error C2440: 'static_cast': cannot convert from 'overloaded-function' to '... (C::*)(...)'
note: None of the functions with this name in scope match the target type

GCC and Clang accept it. In MeshLib this hits e.g. MR::Buffer<T,I>::data():

[[nodiscard]] auto data()       { return data_.get(); }
[[nodiscard]] auto data() const { return data_.get(); }

Minimal repro (Clang 22 and GCC 15 compile; MSVC 19.5x fails), https://gcc.godbolt.org/z/nKnd5ffbE :

template <class T>
struct B {
    T* p = nullptr;
    auto data()       { return p; }
    auto data() const { return p; }
};
auto f() { return static_cast<int* (B<int>::*)()>( &B<int>::data ); }

Every pointer-forming spelling fails the same way on MSVC (static_cast, C-style cast, typed-init of a member pointer) — the failure is resolving the address of the overload set against a target type, not the cast itself.

Fix

Bind the member via a captureless forwarding lambda instead of a pointer-to-member:

+[](_pb11_C const_ &self, /*params*/) -> decltype(auto) { return self.name(/*params*/); }

The lambda calls the function by name on a concrete object, so the const/non-const overload is selected by the object's const-ness and the address of an overloaded member is never taken. Being captureless, it decays (via the leading +) to a function pointer, so it remains a valid non-type template argument to TryAddFunc, and TryAddFunc is unchanged.

This mirrors how friend functions are already bound (DETAIL_MB_PB11_FUNC_PTR_OR_LAMBDA_cl), reuses the existing DETAIL_MB_PB11_MAKE_PARAM_DECLS / _USES helpers, and shares the same documented "extra move per argument" cost. Static methods get the no-self variant calling _pb11_C::name(...).

Scope / notes

  • Touches only DETAIL_MB_PB11_DISPATCH_MEMBER_method in targets/pybind11/core.h. The generated .cpp (from data_to_macros) is unchanged — the same macro invocations now expand to a lambda.
  • Conversion operators (DETAIL_MB_PB11_DISPATCH_MEMBER_conv_op) keep their cast: their return type is explicit (operator T), not deduced, so they don't hit the bug.
  • The lambda calls name_ unqualified to preserve virtual dispatch; this matches the common case where the method is defined in the class being bound. Worth a look for inherited members that are name-hidden in a derived class (the old cast named the base explicitly) — flagging for review.
  • Upstream MSVC bug report: https://developercommunity.visualstudio.com/t/C2440:-static_cast-to-select-an-overload/11107969

Validated end-to-end against MSVC by building MeshLib's Python bindings with MSVC (MeshLib PR #6272).

Fedr and others added 2 commits June 16, 2026 16:49
…r-to-member cast

Member functions were bound by disambiguating overloads with
`static_cast<Ret (C::*)(Args) const>(&C::name)`. MSVC (through 19.5x / VS 2026)
rejects that cast -- "C2440: ... none of the functions with this name in scope
match the target type" -- when the member is overloaded, has a deduced (`auto`)
return type, and the enclosing class is a template (e.g. MR::Buffer<T,I>::data(),
which has auto + const/non-const overloads). GCC and Clang accept it.

Bind via a captureless forwarding lambda instead: it selects the overload by the
object's const-ness and never takes the address of an overloaded member. The
lambda decays to a function pointer, so it stays a valid non-type template
argument for TryAddFunc. This mirrors how friend functions are already bound
(DETAIL_MB_PB11_FUNC_PTR_OR_LAMBDA_cl) and shares its extra-move-per-argument cost.

MRE (Clang/GCC accept, MSVC fails): https://gcc.godbolt.org/z/nKnd5ffbE

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fedr and others added 2 commits June 16, 2026 17:35
…not the bare name

The forwarding-lambda member binding called `self.name(args)`, where `name` is the bare
declaration name. For template member functions whose template parameter is not deducible
from the arguments (e.g. MR::VisualObject::tryGet<T>()), that fails to compile -- the old
static_cast pinned the instantiation via the target signature, but a by-name call must name
it explicitly. Call through `fullname_` instead, which carries the template arguments
(`tryGet<MR::Foo>`); for non-template members it is just the bare name, so nothing else changes.
`_pb11_C` is a concrete (non-dependent) type alias, so no `template` disambiguator is needed.

Caught by building MeshLib's Python bindings (the previously-working Clang path regressed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…plate limitation

The existing note says the template-template-parameter specializations "don't work at all"
on MSVC (MSVC's P0522 relaxed-template-template-argument matching is incomplete for partial
specializations). Reported upstream:
https://developercommunity.visualstudio.com/t/Partial-specialization-with-a-single-arg/11108062

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant