diff --git a/docs/advanced/cast/stl.rst b/docs/advanced/cast/stl.rst index 47c2a96705..304c8cc315 100644 --- a/docs/advanced/cast/stl.rst +++ b/docs/advanced/cast/stl.rst @@ -46,8 +46,23 @@ types: }} The above should be placed in a header file and included in all translation units -where automatic conversion is needed. Similarly, a specialization can be provided -for custom variant types: +where automatic conversion is needed. If the type provides a ``reset`` member +function, that is used to clear the stored value; otherwise, it is cleared by +assigning ``{}`` to it. If neither approach is suitable, ``optional_reset`` can +be specialized to provide an alternative. Here is how pybind11 specializes it for +``std::experimental::optional<>`` (which does not have a ``reset`` method): + +.. code-block:: cpp + + namespace pybind11 { namespace detail { + template struct optional_reset> { + static void reset(std::experimental::optional &value) { + value = std::experimental::nullopt; + } + }; + }} // namespace pybind11::detail + +Similarly, a specialization can be provided for custom variant types: .. code-block:: cpp diff --git a/include/pybind11/stl.h b/include/pybind11/stl.h index f37dd795e3..740d94bf58 100644 --- a/include/pybind11/stl.h +++ b/include/pybind11/stl.h @@ -17,6 +17,7 @@ #include #include #include +#include #if defined(_MSC_VER) #pragma warning(push) @@ -228,6 +229,26 @@ template struct template struct type_caster> : map_caster, Key, Value> { }; +/// Helper class to reset an std::optional-like class in the best way. +template struct optional_reset { +private: + // Either or both of these overloads may exist (SFINAE decides), and if + // both exist then the .reset version is preferred due to a better match + // in the second argument. + template + static decltype(std::declval().reset()) reset_impl(T &value, int) { + return value.reset(); + } + + template + static decltype(std::declval() = {}) reset_impl(T &value, long) { + return value = {}; + } + +public: + static void reset(T &value) { reset_impl(value, 0); } +}; + // This type caster is intended to be used for std::optional and std::experimental::optional template struct optional_caster { using value_conv = make_caster; @@ -242,7 +263,7 @@ template struct optional_caster { if (!src) { return false; } else if (src.is_none()) { - value = {}; // nullopt + optional_reset::reset(value); return true; } value_conv inner_caster; @@ -265,11 +286,20 @@ template<> struct type_caster #endif #if PYBIND11_HAS_EXP_OPTIONAL +// std::experimental::optional doesn't have a reset method, and the value = {} +// strategy can fail if the underlying type isn't move-assignable. +template struct optional_reset> { + static void reset(std::experimental::optional &value) { + value = std::experimental::nullopt; + } +}; + template struct type_caster> : public optional_caster> {}; template<> struct type_caster : public void_caster {}; + #endif /// Visit a variant and cast any found type to Python diff --git a/tests/test_python_types.cpp b/tests/test_python_types.cpp index f9bda617cd..f3375c67e5 100644 --- a/tests/test_python_types.cpp +++ b/tests/test_python_types.cpp @@ -188,6 +188,18 @@ struct MoveOutContainer { struct UnregisteredType { }; +// Class that can be move- and copy-constructed, but not assigned +struct NoAssign { + int value; + + explicit NoAssign(int value = 0) : value(value) {} + NoAssign(const NoAssign &) = default; + NoAssign(NoAssign &&) = default; + + NoAssign &operator=(const NoAssign &) = delete; + NoAssign &operator=(NoAssign &&) = delete; +}; + test_initializer python_types([](py::module &m) { /* No constructor is explicitly defined below. An exception is raised when trying to construct it directly from Python */ @@ -236,6 +248,10 @@ test_initializer python_types([](py::module &m) { py::print("{a} + {b} = {c}"_s.format("a"_a="py::print", "b"_a="str.format", "c"_a="this")); }); + py::class_(m, "NoAssign", "Class with no C++ assignment operators") + .def(py::init<>()) + .def(py::init()); + m.def("test_print_failure", []() { py::print(42, UnregisteredType()); }); #if !defined(NDEBUG) m.attr("debug_enabled") = true; @@ -326,6 +342,7 @@ test_initializer python_types([](py::module &m) { #ifdef PYBIND11_HAS_OPTIONAL has_optional = true; using opt_int = std::optional; + using opt_no_assign = std::optional; m.def("double_or_zero", [](const opt_int& x) -> int { return x.value_or(0) * 2; }); @@ -335,11 +352,15 @@ test_initializer python_types([](py::module &m) { m.def("test_nullopt", [](opt_int x) { return x.value_or(42); }, py::arg_v("x", std::nullopt, "None")); + m.def("test_no_assign", [](const opt_no_assign &x) { + return x ? x->value : 42; + }, py::arg_v("x", std::nullopt, "None")); #endif #ifdef PYBIND11_HAS_EXP_OPTIONAL has_exp_optional = true; using exp_opt_int = std::experimental::optional; + using exp_opt_no_assign = std::experimental::optional; m.def("double_or_zero_exp", [](const exp_opt_int& x) -> int { return x.value_or(0) * 2; }); @@ -349,6 +370,9 @@ test_initializer python_types([](py::module &m) { m.def("test_nullopt_exp", [](exp_opt_int x) { return x.value_or(42); }, py::arg_v("x", std::experimental::nullopt, "None")); + m.def("test_no_assign_exp", [](const exp_opt_no_assign &x) { + return x ? x->value : 42; + }, py::arg_v("x", std::experimental::nullopt, "None")); #endif m.attr("has_optional") = has_optional; diff --git a/tests/test_python_types.py b/tests/test_python_types.py index e427c4677b..f7c3d5555c 100644 --- a/tests/test_python_types.py +++ b/tests/test_python_types.py @@ -337,7 +337,8 @@ def func(self, x, *args): @pytest.mark.skipif(not has_optional, reason='no ') def test_optional(): - from pybind11_tests import double_or_zero, half_or_none, test_nullopt + from pybind11_tests import (double_or_zero, half_or_none, test_nullopt, + test_no_assign, NoAssign) assert double_or_zero(None) == 0 assert double_or_zero(42) == 84 @@ -352,10 +353,16 @@ def test_optional(): assert test_nullopt(42) == 42 assert test_nullopt(43) == 43 + assert test_no_assign() == 42 + assert test_no_assign(None) == 42 + assert test_no_assign(NoAssign(43)) == 43 + pytest.raises(TypeError, test_no_assign, 43) + @pytest.mark.skipif(not has_exp_optional, reason='no ') def test_exp_optional(): - from pybind11_tests import double_or_zero_exp, half_or_none_exp, test_nullopt_exp + from pybind11_tests import (double_or_zero_exp, half_or_none_exp, test_nullopt_exp, + test_no_assign_exp, NoAssign) assert double_or_zero_exp(None) == 0 assert double_or_zero_exp(42) == 84 @@ -370,6 +377,11 @@ def test_exp_optional(): assert test_nullopt_exp(42) == 42 assert test_nullopt_exp(43) == 43 + assert test_no_assign_exp() == 42 + assert test_no_assign_exp(None) == 42 + assert test_no_assign_exp(NoAssign(43)) == 43 + pytest.raises(TypeError, test_no_assign_exp, 43) + @pytest.mark.skipif(not hasattr(pybind11_tests, "load_variant"), reason='no ') def test_variant(doc):