Skip to content

Commit 6a308a4

Browse files
committed
Improve support for std::optional-like classes
With older versions of Boost, `value = {}` can fail to compile because the interpretation of `{}` is ambiguous (could construct `boost::none` or the value type). It can also fail with `std::experimental::optional`, at least on GCC 5.4, because it would construct an instance of the optional type and then move-assign it, and if the value type isn't move assignable this would fail. This is replaced by preferring `.reset` if the type supports it, and also providing a traits class that can be extended to override the behaviour where necessary (which is done for std::experimental::optional). Additionally, the assignment of the value from the inner caster was by copy rather than move, which prevented use with uncopyable types. Closes pybind#847.
1 parent 4567f1f commit 6a308a4

File tree

4 files changed

+87
-6
lines changed

4 files changed

+87
-6
lines changed

docs/advanced/cast/stl.rst

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,23 @@ types:
4646
}}
4747
4848
The above should be placed in a header file and included in all translation units
49-
where automatic conversion is needed. Similarly, a specialization can be provided
50-
for custom variant types:
49+
where automatic conversion is needed. If the type provides a ``reset`` member
50+
function, that is used to clear the stored value; otherwise, it is cleared by
51+
assigning ``{}`` to it. If neither approach is suitable, ``optional_reset`` can
52+
be specialized to provide an alternative. Here is how pybind11 specializes it for
53+
``std::experimental::optional<>`` (which does not have a ``reset`` method):
54+
55+
.. code-block:: cpp
56+
57+
namespace pybind11 { namespace detail {
58+
template<typename T> struct optional_reset<std::experimental::optional<T>> {
59+
static void reset(std::experimental::optional<T> &value) {
60+
value = std::experimental::nullopt;
61+
}
62+
};
63+
}} // namespace pybind11::detail
64+
65+
Similarly, a specialization can be provided for custom variant types:
5166

5267
.. code-block:: cpp
5368

include/pybind11/stl.h

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <iostream>
1818
#include <list>
1919
#include <valarray>
20+
#include <utility>
2021

2122
#if defined(_MSC_VER)
2223
#pragma warning(push)
@@ -228,6 +229,26 @@ template <typename Key, typename Value, typename Compare, typename Alloc> struct
228229
template <typename Key, typename Value, typename Hash, typename Equal, typename Alloc> struct type_caster<std::unordered_map<Key, Value, Hash, Equal, Alloc>>
229230
: map_caster<std::unordered_map<Key, Value, Hash, Equal, Alloc>, Key, Value> { };
230231

232+
/// Helper class to reset an std::optional-like class in the best way.
233+
template<typename T> struct optional_reset {
234+
private:
235+
// Either or both of these overloads may exist (SFINAE decides), and if
236+
// both exist then the .reset version is preferred due to a better match
237+
// in the second argument.
238+
template<typename T2 = T>
239+
static decltype(std::declval<T2>().reset()) reset_impl(T &value, int) {
240+
return value.reset();
241+
}
242+
243+
template<typename T2 = T>
244+
static decltype(std::declval<T2>() = {}) reset_impl(T &value, long) {
245+
return value = {};
246+
}
247+
248+
public:
249+
static void reset(T &value) { reset_impl(value, 0); }
250+
};
251+
231252
// This type caster is intended to be used for std::optional and std::experimental::optional
232253
template<typename T> struct optional_caster {
233254
using value_conv = make_caster<typename T::value_type>;
@@ -242,14 +263,14 @@ template<typename T> struct optional_caster {
242263
if (!src) {
243264
return false;
244265
} else if (src.is_none()) {
245-
value = {}; // nullopt
266+
optional_reset<T>::reset(value);
246267
return true;
247268
}
248269
value_conv inner_caster;
249270
if (!inner_caster.load(src, convert))
250271
return false;
251272

252-
value.emplace(cast_op<typename T::value_type>(inner_caster));
273+
value.emplace(std::move(cast_op<typename T::value_type>(inner_caster)));
253274
return true;
254275
}
255276

@@ -265,11 +286,20 @@ template<> struct type_caster<std::nullopt_t>
265286
#endif
266287

267288
#if PYBIND11_HAS_EXP_OPTIONAL
289+
// std::experimental::optional doesn't have a reset method, and the value = {}
290+
// strategy can fail if the underlying type isn't move-assignable.
291+
template<typename T> struct optional_reset<std::experimental::optional<T>> {
292+
static void reset(std::experimental::optional<T> &value) {
293+
value = std::experimental::nullopt;
294+
}
295+
};
296+
268297
template<typename T> struct type_caster<std::experimental::optional<T>>
269298
: public optional_caster<std::experimental::optional<T>> {};
270299

271300
template<> struct type_caster<std::experimental::nullopt_t>
272301
: public void_caster<std::experimental::nullopt_t> {};
302+
273303
#endif
274304

275305
/// Visit a variant and cast any found type to Python

tests/test_python_types.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,18 @@ struct MoveOutContainer {
188188

189189
struct UnregisteredType { };
190190

191+
// Class that can be move-constructed, but not copied or move-assigned
192+
struct NoMoveAssign {
193+
int value;
194+
195+
explicit NoMoveAssign(int value = 0) : value(value) {}
196+
NoMoveAssign(NoMoveAssign &&) = default;
197+
198+
NoMoveAssign(const NoMoveAssign &) = delete;
199+
NoMoveAssign &operator=(const NoMoveAssign &) = delete;
200+
NoMoveAssign &operator=(NoMoveAssign &&) = delete;
201+
};
202+
191203
test_initializer python_types([](py::module &m) {
192204
/* No constructor is explicitly defined below. An exception is raised when
193205
trying to construct it directly from Python */
@@ -236,6 +248,10 @@ test_initializer python_types([](py::module &m) {
236248
py::print("{a} + {b} = {c}"_s.format("a"_a="py::print", "b"_a="str.format", "c"_a="this"));
237249
});
238250

251+
py::class_<NoMoveAssign>(m, "NoMoveAssign", "Class that can only be move constructed")
252+
.def(py::init<>())
253+
.def(py::init<int>());
254+
239255
m.def("test_print_failure", []() { py::print(42, UnregisteredType()); });
240256
#if !defined(NDEBUG)
241257
m.attr("debug_enabled") = true;
@@ -326,6 +342,7 @@ test_initializer python_types([](py::module &m) {
326342
#ifdef PYBIND11_HAS_OPTIONAL
327343
has_optional = true;
328344
using opt_int = std::optional<int>;
345+
using opt_no_move_assign = std::optional<NoMoveAssign>;
329346
m.def("double_or_zero", [](const opt_int& x) -> int {
330347
return x.value_or(0) * 2;
331348
});
@@ -335,11 +352,15 @@ test_initializer python_types([](py::module &m) {
335352
m.def("test_nullopt", [](opt_int x) {
336353
return x.value_or(42);
337354
}, py::arg_v("x", std::nullopt, "None"));
355+
m.def("test_no_move_assign", [](const opt_no_move_assign &x) {
356+
return x ? x.value : 42;
357+
}, py::argv_("x", std::nullopt, "None"));
338358
#endif
339359

340360
#ifdef PYBIND11_HAS_EXP_OPTIONAL
341361
has_exp_optional = true;
342362
using exp_opt_int = std::experimental::optional<int>;
363+
using exp_opt_no_move_assign = std::experimental::optional<NoMoveAssign>;
343364
m.def("double_or_zero_exp", [](const exp_opt_int& x) -> int {
344365
return x.value_or(0) * 2;
345366
});
@@ -349,6 +370,9 @@ test_initializer python_types([](py::module &m) {
349370
m.def("test_nullopt_exp", [](exp_opt_int x) {
350371
return x.value_or(42);
351372
}, py::arg_v("x", std::experimental::nullopt, "None"));
373+
m.def("test_no_move_assign_exp", [](const exp_opt_no_move_assign &x) {
374+
return x ? x->value : 42;
375+
}, py::arg_v("x", std::experimental::nullopt, "None"));
352376
#endif
353377

354378
m.attr("has_optional") = has_optional;

tests/test_python_types.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,8 @@ def func(self, x, *args):
337337

338338
@pytest.mark.skipif(not has_optional, reason='no <optional>')
339339
def test_optional():
340-
from pybind11_tests import double_or_zero, half_or_none, test_nullopt
340+
from pybind11_tests import (double_or_zero, half_or_none, test_nullopt,
341+
test_no_move_assign, NoMoveAssign)
341342

342343
assert double_or_zero(None) == 0
343344
assert double_or_zero(42) == 84
@@ -352,10 +353,16 @@ def test_optional():
352353
assert test_nullopt(42) == 42
353354
assert test_nullopt(43) == 43
354355

356+
assert test_no_move_assign() == 42
357+
assert test_no_move_assign(None) == 42
358+
assert test_no_move_assign(NoMoveAssign(43)) == 43
359+
pytest.raises(TypeError, test_no_move_assign, 43)
360+
355361

356362
@pytest.mark.skipif(not has_exp_optional, reason='no <experimental/optional>')
357363
def test_exp_optional():
358-
from pybind11_tests import double_or_zero_exp, half_or_none_exp, test_nullopt_exp
364+
from pybind11_tests import (double_or_zero_exp, half_or_none_exp, test_nullopt_exp,
365+
test_no_move_assign_exp, NoMoveAssign)
359366

360367
assert double_or_zero_exp(None) == 0
361368
assert double_or_zero_exp(42) == 84
@@ -370,6 +377,11 @@ def test_exp_optional():
370377
assert test_nullopt_exp(42) == 42
371378
assert test_nullopt_exp(43) == 43
372379

380+
assert test_no_move_assign_exp() == 42
381+
assert test_no_move_assign_exp(None) == 42
382+
assert test_no_move_assign_exp(NoMoveAssign(43)) == 43
383+
pytest.raises(TypeError, test_no_move_assign_exp, 43)
384+
373385

374386
@pytest.mark.skipif(not hasattr(pybind11_tests, "load_variant"), reason='no <variant>')
375387
def test_variant(doc):

0 commit comments

Comments
 (0)