-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Prefer non-converting argument overloads #643
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
Conversation
Arithmetic and complex casters now only do a converting cast when `convert=true`; previously they would convert always (e.g. when passing an int to a float-accepting function, or a float to complex-accepting function).
This adds support for controlling the `convert` flag of arguments through the py::arg annotation. This then allows arguments to be flagged as non-converting, which the type_caster is able to use to request different behaviour. Currently, AFAICS `convert` is only used for type converters of regular pybind11-registered types; all of the other core type_casters ignore it. We can, however, repurpose it to control internal conversion of converters like Eigen and `array`: most usefully to give callers a way to disable the conversion that would otherwise occur when a `Eigen::Ref<const Eigen::Matrix>` argument is passed a numpy array that requires conversion (either because it has an incompatible stride or the wrong dtype). Specifying a noconvert looks like one of these: m.def("f1", &f, "a"_a.noconvert() = "default"); // Named, default, noconvert m.def("f2", &f, "a"_a.noconvert()); // Named, no default, no converting m.def("f3", &f, py::arg().noconvert()); // Unnamed, no default, no converting (The last part--being able to declare a py::arg without a name--is new: previous py::arg() only accepted named keyword arguments). Such an non-convert argument is then passed `convert = false` by the type caster when loading the argument. Whether this has an effect is up to the type caster itself, but as mentioned above, this would be extremely helpful for the Eigen support to give a nicer way to specify a "no-copy" mode than the custom wrapper in the current PR, and moreover isn't an Eigen-specific hack.
@@ -28,9 +28,10 @@ def test_pointers(msg): | |||
print_opaque_list, return_null_str, get_null_str_value, | |||
return_unique_ptr, ConstructorStats) | |||
|
|||
living_before = ConstructorStats.get(ExampleMandA).alive() |
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.
It isn't entirely obvious why this change, but I was getting false positives of this test triggered by another test failure (the other failure resulting in an ExampleMandA
instance not being destroyed). This just prevents the false positive when a failure is happening elsewhere.
This changes the function dispatching code for overloaded functions into a two-pass procedure where we first try all overloads with `convert=false` for all arguments. If no function calls succeeds in the first pass, we then try a second pass where we allow arguments to have `convert=true` (unless, of course, the argument was explicitly specified with `py::arg().noconvert()`). For non-overloaded methods, the two-pass procedure is skipped (we just make the overload-allowed call). The second pass is also skipped if it would result in the same thing (i.e. where all arguments are `.noconvert()` arguments).
a3405f3
to
e550589
Compare
I think this is great and will solve a number of recurring difficulties with method resolution. It's nice that all of this also integrates with the existing implicit conversions for custom types and extends them to builtin types. My vote would be to merge this one and close the other PR. I find the |
There isn't a leak in the current test code when everything passes, but if some other test fails and leaks, it causes both the earlier test and this one to fail, when really it should be just the other one failing. |
As for merging this instead of the other, it really doesn't make any difference: this includes the other one anyway. |
Merged! |
This is awesome! Actual overload resolution 👍 |
I agree, this is really nice work -- thank you for the beautiful implementation. |
Thinking a bit more about this patch, I was wondering about the following: does it make sense for the |
I'm not sure I follow: (And if the method isn't overloaded, there is no second pass at all: we essentially just skip the first noconvert attempt entirely). I think what you might be getting at is that we can, however, get in a situation where we call an argument's We could possibly avoid that by letting the Now that I've written it up, it doesn't seem all that complicated, I'll whip something up. |
Right! Thinking a bit more about this, I'm starting to get worried about object code size, since this is the sort of thing that will touch every bound function ;). If data has to be moved around in every function call to get this to work, it might be better to tolerate the unnecessary extra |
Yeah, I think you're right: the biggest problem is that every function Given that this only has an effect on overloaded functions, but incurs the cost for all functions, it doesn't seem worthwhile. Better just to sometimes invoke |
Just FYI. We are using custom converters to implement a form of lazy loading of big objects (say pulling from a database). By registering a in implicit conversion from |
If the type isn't an argument to an overloaded function/method, this PR won't change anything. If it is, you could get multiple |
This changes function dispatching to work in two passes for overloaded functions: the first tries calling a function with all arguments loaded with
convert=false
. If this fails for all overloads, a second pass is tried which allows conversion (except for those arguments where it is explicitly disabled withpy::arg().noconvert()
).The main point here is to prefer calls to overloads that don't require conversion over calls that do require conversion, regardless of the order in which the functions were registered. This should allow issues like #631 to be solved relatively easily by not loading when
convert=false
except when the type already matches.For non-overloaded functions, this code doesn't bother with two passes at all (there's no point in first trying a non-converting call because if it fails we'd just immediately make the converting call anyway).
For overloads where there are no converting argument anyway (either no arguments, or all arguments are
py::arg().noconvert()
) we don't bother with the second pass attempt (since it would haveconvert_args
flags exactly identical to the first pass).Without the included tests, this increases the test .so size by 4KB under both GCC and macOS Sierra Clang, but that's a constant increase (i.e. the same increase for binding just one function or the entire test suite).
Note that the first two commits here are just PR #634, which this depends on, but I'm submitting this as a separate PR because it's really a completely different feature and deserves separate consideration/discussion. (Also note that the no-variable-.so increase I mentioned above is relative to #634, i.e. there's a small variable increase relative to master, but that's coming from #634).