-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add argument number dispatch mechanism for std::function casting #5285
base: master
Are you sure you want to change the base?
Conversation
87b9bb2
to
7b9d61f
Compare
I'm wondering about the tradeoff: extra-code-complexity vs. usability-benefit For example:
This could simply become something like:
Meaningful names will make the client code more obvious and readable. When is it important that the names are identical? |
I think you are right and this is one workaround. We used it in our code but we changed the code to just accept a I have more complicated types as arguments, where it does not make much sense to change the function name, e.g. To stay in the example I don't want to write m.def("handle_point2D", [](const std::function<int(int, int)>& f) { return f(2, 3); })
m.def("handle_point3D", [](const std::function<int(int, int, int)>& f) { return f(2, 3, 4); }) but I just want to have a single function So I think the dispatch should be added to really dispatch this case or at least to show an exception that really states the problem, since for me it also took a while to figure out, why it always tries to call the wrong overload. If this is not added it would be nice to explicitly state it in the documentation, that this is not supported. |
Understood, and if the implementation was easier, I'd say fine, why not. I'm worried about relying on I haven't looked in great detail, but I'm pretty sure the error handling is incomplete. This will add to the existing code. So it comes down to judgement, cost vs benefit. I think it'll be "too much code" for "a relative niche feature". But I'm not the only one here: @henryiii @EthanSteinberg @Skylion007 What's your opinion? |
I think this argument number dispatch is a good feature that will make it easier to write pythonic bindings (as python code generally leans in the direction of functions doing type dispatching). I am a bit worried about runtime overhead though. At minimum, we probably only want to do this check if there is more than one overload. Let me give this a proper review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work! Just a couple of minor things that I think need to get fixed.
@@ -75,6 +84,36 @@ struct type_caster<std::function<Return(Args...)>> { | |||
// PYPY segfaults here when passing builtin function like sum. | |||
// Raising an fail exception here works to prevent the segfault, but only on gcc. | |||
// See PR #1413 for full details | |||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To try to minimize the runtime overhead, I would consider adding a rec->next check here to only trigger this if there is a another possible overload to try.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm trying to understand your comment but I'm afraid I don't know what you are suggesting. Can you please elaborate?
include/pybind11/functional.h
Outdated
// Check number of arguments of Python function | ||
auto getArgCount = [&](PyObject *obj) { | ||
// This is faster then doing import inspect and inspect.signature(obj).parameters | ||
auto *t = PyObject_GetAttrString(obj, "__code__"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't this cause a memory leak as GetAttrString returns a new reference, but you never Py_DECREF?
Why not use pybind11's object wrappers to make sure you never leak memory? (https://pybind11.readthedocs.io/en/stable/reference.html#_CPPv46object)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes you are right I used PyObject_GetAttrString
and .attr
where i don't have to check if the attribute.
include/pybind11/functional.h
Outdated
}; | ||
long argCount = -1; | ||
|
||
if (static_cast<bool>(PyObject_HasAttrString(src.ptr(), "__code__"))) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static_cast makes this code look more confusing than it should be. Either just have an if statement directly on HasAttrString or explicitly do if (has_attr_string == 1)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd try to only use PyObject_GetAttrString()
here and not PyObject_HasAttrString()
at all (unless you really never actually need the attribute).
I looked at this in depth in some other context (a couple years ago probably, but I'd bet it's still the same) and found that PyObject_HasAttrString()
uses PyObject_GetAttrString()
and just throws the object away.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General: when you feel you're done, I'd review this code do see what can be factored out to a non-templated inline
function (or functions). That might help the compiler a little, but is also nice for humans reading the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I used PyObject_GetAttrString
and .attr
, where I'm sure that the attribute is existing, since there I didn't want to write try ... catch
around .attr
for performance reasons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I found out how to use getattr
. I hope this is the way to go.
@@ -103,6 +103,20 @@ def test_cpp_callable_cleanup(): | |||
assert alive_counts == [0, 1, 2, 1, 2, 1, 0] | |||
|
|||
|
|||
def test_cpp_correct_overload_resolution(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also add a class test (that uses def call) here as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
include/pybind11/functional.h
Outdated
while (rec != nullptr) { | ||
const int correctingSelfArgument = rec->is_method ? 1 : 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self_offset
preferred as variable name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
include/pybind11/functional.h
Outdated
while (rec != nullptr) { | ||
const int correctingSelfArgument = rec->is_method ? 1 : 0; | ||
if (rec->nargs - correctingSelfArgument != sizeof...(Args)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rec->nargs != sizeof...(Args) + 1
to avoid any doubts/tooling noise around unsigned 0 - 1. (probably doesn't matter, but super easy general defensive approach)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now it reads:
if (rec->nargs != sizeof...(Args) + self_offset)
I assume you indicated like that?
5f66fd1
to
febc0b1
Compare
Thanks for your comments. I tried to refactor accordingly. |
2187294
to
9768d5f
Compare
ddc2aaa
to
ec84c33
Compare
Ok actually I'm wittnessing some problems in different CI configurations. I will let you know when this is ready again |
8a3896e
to
4212339
Compare
375db6a
to
e0be5db
Compare
Ok now this seems ready again. |
(Thanks for all the work. I'll look asap, might take me a couple days.) |
Description
The proposed PR partly fixing function overloads where the arguments are
std::function
s.It fixes #3035.
The problem appears, if we have at least two functions:
and we want to call them in Python
The function
f
is cast to the first overload, which takes two arguments.Then the call fails with
f() takes 1 positional argument but 2 were given
Calling it from c++ with
fails with
This is fixed with this PR.
In Compiler Explorer I created the failing example, see if you want https://godbolt.org/z/c4Y9bTxTE
Suggested changelog entry:
Number of arguments are now checked for casting `std::function`.