Skip to content
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

Merged
merged 6 commits into from
Dec 12, 2016

Conversation

jagerman
Copy link
Member

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 with the appropriate extra arguments.

Fixes #547 (the original post).

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).
Copy link
Member

@dean0x7d dean0x7d left a 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");
Copy link
Member

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;
Copy link
Member

@dean0x7d dean0x7d Dec 10, 2016

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.

@jagerman
Copy link
Member Author

The SFINAE is needed, I think, because we want things like py::int_ to go via the py::object overload, which they won't if the template SFINAE isn't there.

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 cast.h).

@steve-lorimer
Copy link

@jagerman thanks, this is great!

@dean0x7d
Copy link
Member

@jagerman The SFINAE is required, but my main point is that it should be pushed down to py::cast or a new helper function so that it's only done in one place instead of having to repeat it for each interface function. This would simplify the accessor's operator= from 4 to 2 overloads and append from 2 to 1. Similarly new methods would only require 1.

@wjakob
Copy link
Member

wjakob commented Dec 11, 2016

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).
@jagerman
Copy link
Member Author

@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; }
Copy link
Member

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.

Copy link
Member

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).

Copy link
Member Author

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; }
Copy link
Member

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?

Copy link
Member

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);

Copy link
Member Author

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).

@aldanor
Copy link
Member

aldanor commented Dec 12, 2016

  • list::append() was updated, shouldnt set::add() get the same treatment?
  • There's a few more places in the codebase where explicit casting is still used, like here and here, there's probably a few more (it's mostly casts to int_ and str which is easily searchable).

@@ -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); }
Copy link
Member

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; }
Copy link
Member

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; }
Copy link
Member

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)); }
Copy link
Member

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()); }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T&& and forward val.

@jagerman
Copy link
Member Author

Updated with the comments.

The PyObject * overload is still needed: without it, the casting template matches a PyObject * (which is used in many places), but that's wrong (it goes to py::cast(obj) which isn't valid for a PyObject *). In the previous, non-templated code, providing a PyObject * worked by implicit conversion to handle via handle's implicit conversion constructor. With the templated version, even with an explicit handle overload, that doesn't work anymore: the non-pyobject templated method takes precedence since provides an exact match while the handle overload only matches via an implicit conversion.

@jagerman
Copy link
Member Author

  • list::append() was updated, shouldnt set::add() get the same treatment?

Indeed, fixed.

  • There's a few more places in the codebase where explicit casting is still used, like here and here, there's probably a few more (it's mostly casts to int_ and str which is easily searchable).

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.

@wjakob
Copy link
Member

wjakob commented Dec 12, 2016

I think this looks great now -- thank you! I'll go ahead and merge it.

@wjakob wjakob merged commit 3f1ff3f into pybind:master Dec 12, 2016
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.

5 participants