Commit 617fbcfc by Jason Rhinelander Committed by Wenzel Jakob

Fix stl_bind to support movable, non-copyable value types (#490)

This commit includes the following changes:

* Don't provide make_copy_constructor for non-copyable container

make_copy_constructor currently fails for various stl containers (e.g.
std::vector, std::unordered_map, std::deque, etc.) when the container's
value type (e.g. the "T" or the std::pair<K,T> for a map) is
non-copyable.  This adds an override that, for types that look like
containers, also requires that the value_type be copyable.

* stl_bind.h: make bind_{vector,map} work for non-copy-constructible types

Most stl_bind modifiers require copying, so if the type isn't copy
constructible, we provide a read-only interface instead.

In practice, this means that if the type is non-copyable, it will be,
for all intents and purposes, read-only from the Python side (but
currently it simply fails to compile with such a container).

It is still possible for the caller to provide an interface manually
(by defining methods on the returned class_ object), but this isn't
something stl_bind can handle because the C++ code to construct values
is going to be highly dependent on the container value_type.

* stl_bind: copy only for arithmetic value types

For non-primitive types, we may well be copying some complex type, when
returning by reference is more appropriate.  This commit returns by
internal reference for all but basic arithmetic types.

* Return by reference whenever possible

Only if we definitely can't--i.e. std::vector<bool>--because v[i]
returns something that isn't a T& do we copy; for everything else, we
return by reference.

For the map case, we can always return by reference (at least for the
default stl map/unordered_map).
parent 06bd27f5
...@@ -346,6 +346,18 @@ using cast_op_type = typename std::conditional<std::is_pointer<typename std::rem ...@@ -346,6 +346,18 @@ using cast_op_type = typename std::conditional<std::is_pointer<typename std::rem
typename std::add_pointer<intrinsic_t<T>>::type, typename std::add_pointer<intrinsic_t<T>>::type,
typename std::add_lvalue_reference<intrinsic_t<T>>::type>::type; typename std::add_lvalue_reference<intrinsic_t<T>>::type>::type;
// std::is_copy_constructible isn't quite enough: it lets std::vector<T> (and similar) through when
// T is non-copyable, but code containing such a copy constructor fails to actually compile.
template <typename T, typename SFINAE = void> struct is_copy_constructible : std::is_copy_constructible<T> {};
// Specialization for types that appear to be copy constructible but also look like stl containers
// (we specifically check for: has `value_type` and `reference` with `reference = value_type&`): if
// so, copy constructability depends on whether the value_type is copy constructible.
template <typename Container> struct is_copy_constructible<Container, enable_if_t<
std::is_copy_constructible<Container>::value &&
std::is_same<typename Container::value_type &, typename Container::reference>::value
>> : std::is_copy_constructible<typename Container::value_type> {};
/// Generic type caster for objects stored on the heap /// Generic type caster for objects stored on the heap
template <typename type> class type_caster_base : public type_caster_generic { template <typename type> class type_caster_base : public type_caster_generic {
using itype = intrinsic_t<type>; using itype = intrinsic_t<type>;
...@@ -383,20 +395,21 @@ protected: ...@@ -383,20 +395,21 @@ protected:
#if !defined(_MSC_VER) #if !defined(_MSC_VER)
/* Only enabled when the types are {copy,move}-constructible *and* when the type /* Only enabled when the types are {copy,move}-constructible *and* when the type
does not have a private operator new implementaton. */ does not have a private operator new implementaton. */
template <typename T = type> static auto make_copy_constructor(const T *value) -> decltype(new T(*value), Constructor(nullptr)) { template <typename T = type, typename = enable_if_t<is_copy_constructible<T>::value>> static auto make_copy_constructor(const T *value) -> decltype(new T(*value), Constructor(nullptr)) {
return [](const void *arg) -> void * { return new T(*((const T *) arg)); }; } return [](const void *arg) -> void * { return new T(*((const T *) arg)); }; }
template <typename T = type> static auto make_move_constructor(const T *value) -> decltype(new T(std::move(*((T *) value))), Constructor(nullptr)) { template <typename T = type> static auto make_move_constructor(const T *value) -> decltype(new T(std::move(*((T *) value))), Constructor(nullptr)) {
return [](const void *arg) -> void * { return (void *) new T(std::move(*((T *) arg))); }; } return [](const void *arg) -> void * { return (void *) new T(std::move(*((T *) arg))); }; }
#else #else
/* Visual Studio 2015's SFINAE implementation doesn't yet handle the above robustly in all situations. /* Visual Studio 2015's SFINAE implementation doesn't yet handle the above robustly in all situations.
Use a workaround that only tests for constructibility for now. */ Use a workaround that only tests for constructibility for now. */
template <typename T = type, typename = enable_if_t<std::is_copy_constructible<T>::value>> template <typename T = type, typename = enable_if_t<is_copy_constructible<T>::value>>
static Constructor make_copy_constructor(const T *value) { static Constructor make_copy_constructor(const T *value) {
return [](const void *arg) -> void * { return new T(*((const T *)arg)); }; } return [](const void *arg) -> void * { return new T(*((const T *)arg)); }; }
template <typename T = type, typename = enable_if_t<std::is_move_constructible<T>::value>> template <typename T = type, typename = enable_if_t<std::is_move_constructible<T>::value>>
static Constructor make_move_constructor(const T *value) { static Constructor make_move_constructor(const T *value) {
return [](const void *arg) -> void * { return (void *) new T(std::move(*((T *)arg))); }; } return [](const void *arg) -> void * { return (void *) new T(std::move(*((T *)arg))); }; }
#endif #endif
static Constructor make_copy_constructor(...) { return nullptr; } static Constructor make_copy_constructor(...) { return nullptr; }
static Constructor make_move_constructor(...) { return nullptr; } static Constructor make_move_constructor(...) { return nullptr; }
}; };
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
#include <pybind11/stl_bind.h> #include <pybind11/stl_bind.h>
#include <map> #include <map>
#include <deque>
#include <unordered_map> #include <unordered_map>
class El { class El {
...@@ -26,6 +27,32 @@ std::ostream & operator<<(std::ostream &s, El const&v) { ...@@ -26,6 +27,32 @@ std::ostream & operator<<(std::ostream &s, El const&v) {
return s; return s;
} }
/// Issue #487: binding std::vector<E> with E non-copyable
class E_nc {
public:
explicit E_nc(int i) : value{i} {}
E_nc(const E_nc &) = delete;
E_nc &operator=(const E_nc &) = delete;
E_nc(E_nc &&) = default;
E_nc &operator=(E_nc &&) = default;
int value;
};
template <class Container> Container *one_to_n(int n) {
auto v = new Container();
for (int i = 1; i <= n; i++)
v->emplace_back(i);
return v;
}
template <class Map> Map *times_ten(int n) {
auto m = new Map();
for (int i = 1; i <= n; i++)
m->emplace(int(i), E_nc(10*i));
return m;
}
test_initializer stl_binder_vector([](py::module &m) { test_initializer stl_binder_vector([](py::module &m) {
py::class_<El>(m, "El") py::class_<El>(m, "El")
.def(py::init<int>()); .def(py::init<int>());
...@@ -36,6 +63,7 @@ test_initializer stl_binder_vector([](py::module &m) { ...@@ -36,6 +63,7 @@ test_initializer stl_binder_vector([](py::module &m) {
py::bind_vector<std::vector<El>>(m, "VectorEl"); py::bind_vector<std::vector<El>>(m, "VectorEl");
py::bind_vector<std::vector<std::vector<El>>>(m, "VectorVectorEl"); py::bind_vector<std::vector<std::vector<El>>>(m, "VectorVectorEl");
}); });
test_initializer stl_binder_map([](py::module &m) { test_initializer stl_binder_map([](py::module &m) {
...@@ -44,4 +72,24 @@ test_initializer stl_binder_map([](py::module &m) { ...@@ -44,4 +72,24 @@ test_initializer stl_binder_map([](py::module &m) {
py::bind_map<std::map<std::string, double const>>(m, "MapStringDoubleConst"); py::bind_map<std::map<std::string, double const>>(m, "MapStringDoubleConst");
py::bind_map<std::unordered_map<std::string, double const>>(m, "UnorderedMapStringDoubleConst"); py::bind_map<std::unordered_map<std::string, double const>>(m, "UnorderedMapStringDoubleConst");
});
test_initializer stl_binder_noncopyable([](py::module &m) {
py::class_<E_nc>(m, "ENC")
.def(py::init<int>())
.def_readwrite("value", &E_nc::value);
py::bind_vector<std::vector<E_nc>>(m, "VectorENC");
m.def("get_vnc", &one_to_n<std::vector<E_nc>>, py::return_value_policy::reference);
py::bind_vector<std::deque<E_nc>>(m, "DequeENC");
m.def("get_dnc", &one_to_n<std::deque<E_nc>>, py::return_value_policy::reference);
py::bind_map<std::map<int, E_nc>>(m, "MapENC");
m.def("get_mnc", &times_ten<std::map<int, E_nc>>, py::return_value_policy::reference);
py::bind_map<std::unordered_map<int, E_nc>>(m, "UmapENC");
m.def("get_umnc", &times_ten<std::unordered_map<int, E_nc>>, py::return_value_policy::reference);
}); });
...@@ -50,7 +50,6 @@ def test_vector_bool(): ...@@ -50,7 +50,6 @@ def test_vector_bool():
assert vv_c[i] == (i % 2 == 0) assert vv_c[i] == (i % 2 == 0)
assert str(vv_c) == "VectorBool[1, 0, 1, 0, 1, 0, 1, 0, 1, 0]" assert str(vv_c) == "VectorBool[1, 0, 1, 0, 1, 0, 1, 0, 1, 0]"
def test_map_string_double(): def test_map_string_double():
from pybind11_tests import MapStringDouble, UnorderedMapStringDouble from pybind11_tests import MapStringDouble, UnorderedMapStringDouble
...@@ -97,3 +96,58 @@ def test_map_string_double_const(): ...@@ -97,3 +96,58 @@ def test_map_string_double_const():
umc['b'] = 21.5 umc['b'] = 21.5
str(umc) str(umc)
def test_noncopyable_vector():
from pybind11_tests import ENC, get_vnc
vnc = get_vnc(5)
for i in range(0, 5):
assert(vnc[i].value == i+1)
i = 1
for j in vnc:
assert(j.value == i)
i += 1
def test_noncopyable_deque():
from pybind11_tests import ENC, get_dnc
dnc = get_dnc(5)
for i in range(0, 5):
assert(dnc[i].value == i+1)
i = 1
for j in dnc:
assert(j.value == i)
i += 1
def test_noncopyable_map():
from pybind11_tests import ENC, get_mnc
mnc = get_mnc(5)
for i in range(1, 6):
assert(mnc[i].value == 10*i)
i = 1
vsum = 0
for k, v in mnc.items():
assert(v.value == 10*k)
vsum += v.value
assert(vsum == 150)
def test_noncopyable_unordered_map():
from pybind11_tests import ENC, get_umnc
mnc = get_umnc(5)
for i in range(1, 6):
assert(mnc[i].value == 10*i)
i = 1
vsum = 0
for k, v in mnc.items():
assert(v.value == 10*k)
vsum += v.value
assert(vsum == 150)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment