-
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
Adds automatic casting on assignment of non-pyobject types #551
Conversation
This adds automatic casting when assigning to python types like dict, list, and attributes. Instead of: dict["key"] = py::cast(val); m.attr("foo") = py::cast(true); list.append(py::cast(42)); you can now simply write: dict["key"] = val; m.attr("foo") = true; list.append(42); Casts needing extra parameters (e.g. for a non-default rvp) still require the py::cast() call. Fixes pybind#547 (the original post).
ddc4868
to
fb26eac
Compare
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.
Great, this should make the interface a lot easier to use.
My only concern is that having both the template SFINAE overload and regular handle
overload may be too much bookkeeping if/when the C++ API is extended with new methods (list.insert
, dict.setdefault
and similar). See my comment in the code.
m.attr("the_answer") = py::cast(42); | ||
m.attr("what") = py::cast("World"); | ||
m.attr("the_answer") = 42; | ||
auto world = py::cast("World"); |
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.
Perhaps use py::object
here so readers can get a better idea of what py::cast
is doing.
@@ -774,6 +782,8 @@ class list : public object { | |||
size_t size() const { return (size_t) PyList_Size(m_ptr); } | |||
detail::list_accessor operator[](size_t index) const { return {*this, index}; } | |||
void append(handle h) const { PyList_Append(m_ptr, h.ptr()); } | |||
template <typename T, detail::enable_if_t<detail::is_implicitly_castable<T>::value, int> = 0> | |||
void append(const T &value) const; |
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 wonder if this and operator=
can be streamlined a bit so that they don't need both the handle
and template T
overloads? Having just the template overload without the extra SFINAE would be ideal IMO.
I guess the main issue here is that a py::cast
on handle
would introduce an unnecessary incref/decref
, so perhaps there should be something more specialized here. This could be useful for future expansion of the py::object
-derived classes. There are lots of methods which could be exposed in C++ similarly to list.append()
and it would be unfortunate to have to create 2 overloads for each new method.
The SFINAE is needed, I think, because we want things like I'm open to other suggestions, of course; it is indeed a bit cumbersome (especially because it needs to be duplicated again with the actual implementation in |
@jagerman thanks, this is great! |
@jagerman The SFINAE is required, but my main point is that it should be pushed down to |
I agree with @dean0x7d: the thought to duplicate all convenience functions in future expansions of various pytypes.h classes seems worrisome. It would be nicer to push the SFINAE into one centralized place. |
Combined non-converting handle and autocasting template methods via a helper method that either just returns (handle) or casts (C++ type).
@dean0x7d - take a look at the change I just pushed, I think that is basically what you had in mind, and I definitely agree it's a nicer approach. |
// When given a pyobject, this simply returns the pyobject as-is; for other C++ type, the value goes | ||
// through pybind11::cast(obj) to convert it to an `object`. | ||
template <typename T, enable_if_t<is_pyobject<T>::value, int> = 0> | ||
T object_or_cast(T o) { return o; } |
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.
Would it make sense to accept an universal reference as T here? In that case, I think that values could be moved into the object_or_cast
parameter, avoiding a useless incref/decref.
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.
This overload can do pure forwarding:
auto object_or_cast(T &&o) -> decltype(std::forward<T>(o)) { return std::forward<T>(o); }
Here, the trailing decltype
is better than a T
return type because it will just do forwarding without generating any copy/move constructors (even if the compiler can optimize them away, why give it extra work).
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've changed all of them to use universal references.
template <typename T, enable_if_t<!is_pyobject<T>::value && !std::is_same<T, PyObject*>::value, int> = 0> | ||
object object_or_cast(const T &o); | ||
// Match a PyObject*, which we want to convert directly to handle via its converting constructor | ||
template <typename T, enable_if_t<std::is_same<T, PyObject>::value, int> = 0> handle object_or_cast(T *ptr) { return ptr; } |
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 wasn't clear to me why the return value is handle
here. Could the reinterpret_borrow
be avoided below if we return an object here? Do we even need the PyObject*
version?
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.
The reinterpret_borrow
is still needed for the previous overload which can also return (forward) a handle
.
I don't think the PyObject*
overload is needed. The forwarding overload can take care of it.
BTW:
template <typename T, enable_if_t<std::is_same<T, PyObject>::value, int> = 0> handle object_or_cast(T *ptr);
is equivalent to:
inline handle object_or_cast(PyObject *ptr);
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 switched the overload to the latter. It's still needed, though (see my comment in the PR).
@@ -1120,6 +1120,10 @@ template <> inline void object::cast() && { return; } | |||
|
|||
NAMESPACE_BEGIN(detail) | |||
|
|||
// Declared in pytypes.h: | |||
template <typename T, enable_if_t<!is_pyobject<T>::value && !std::is_same<T, PyObject *>::value, int>> | |||
object object_or_cast(const T &o) { return pybind11::cast(o); } |
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.
Should use forwarding: object_or_cast(T &&x) { return pybind11::cast(std::forward<T>(x)); }
// When given a pyobject, this simply returns the pyobject as-is; for other C++ type, the value goes | ||
// through pybind11::cast(obj) to convert it to an `object`. | ||
template <typename T, enable_if_t<is_pyobject<T>::value, int> = 0> | ||
T object_or_cast(T o) { return o; } |
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.
This overload can do pure forwarding:
auto object_or_cast(T &&o) -> decltype(std::forward<T>(o)) { return std::forward<T>(o); }
Here, the trailing decltype
is better than a T
return type because it will just do forwarding without generating any copy/move constructors (even if the compiler can optimize them away, why give it extra work).
template <typename T, enable_if_t<!is_pyobject<T>::value && !std::is_same<T, PyObject*>::value, int> = 0> | ||
object object_or_cast(const T &o); | ||
// Match a PyObject*, which we want to convert directly to handle via its converting constructor | ||
template <typename T, enable_if_t<std::is_same<T, PyObject>::value, int> = 0> handle object_or_cast(T *ptr) { return ptr; } |
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.
The reinterpret_borrow
is still needed for the previous overload which can also return (forward) a handle
.
I don't think the PyObject*
overload is needed. The forwarding overload can take care of it.
BTW:
template <typename T, enable_if_t<std::is_same<T, PyObject>::value, int> = 0> handle object_or_cast(T *ptr);
is equivalent to:
inline handle object_or_cast(PyObject *ptr);
template <typename T, enable_if_t<!std::is_base_of<object, T>::value && !std::is_base_of<accessor, T>::value, int> = 0> | ||
void operator=(const T &value) && { Policy::set(obj, key, object_or_cast(value)); } | ||
template <typename T, enable_if_t<!std::is_base_of<object, T>::value && !std::is_base_of<accessor, T>::value, int> = 0> | ||
void operator=(const T value) & { get_cache() = reinterpret_borrow<object>(object_or_cast(value)); } |
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 believe the const object &
overload can be removed because the template will take care of it. The const accessor &
overload must stay in order to override the default assignment operator (templates are not allowed to replace default compiler generated assignments).
The SFINAE can go away because it's handled by normal overload resolution (given an exact match between a template and a non-template function, the non-template function always has priority).
Forward value
in the template.
@@ -773,7 +787,7 @@ class list : public object { | |||
} | |||
size_t size() const { return (size_t) PyList_Size(m_ptr); } | |||
detail::list_accessor operator[](size_t index) const { return {*this, index}; } | |||
void append(handle h) const { PyList_Append(m_ptr, h.ptr()); } | |||
template <typename T> void append(const T &val) const { PyList_Append(m_ptr, detail::object_or_cast(val).ptr()); } |
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.
T&&
and forward val
.
Updated with the comments. The |
Indeed, fixed.
Do you think it's worth changing them all? There's no real advantage to implicit casting other than coding convenience, but existing code is already written (and keeping at least some of it, particularly in the test code, helps ensure that it still works as expected). The point of this wasn't really to eliminate explicit casting, but just to allow implicit casting as an alternative in common cases. |
I think this looks great now -- thank you! I'll go ahead and merge it. |
This adds automatic casting when assigning to python types like dict, list, and attributes. Instead of:
you can now simply write:
Casts needing extra parameters (e.g. for a non-default rvp) still require the
py::cast()
call with the appropriate extra arguments.Fixes #547 (the original post).