Skip to content

Commit 362f69e

Browse files
fix: the types for return_value_policy_override in optional_caster
`return_value_policy_override` was not being applied correctly in `optional_caster` in two ways: - The `is_lvalue_reference` condition referenced `T`, which was the `optional<T>` type parameter from the class, when it should have used `T_`, which was the parameter to the `cast` function. `T_` can potentially be a reference type, but `T` will never be. - The type parameter passed to `return_value_policy_override` should be `T::value_type`, not `T`. This matches the way that the other STL container type casters work. The result of these issues was that a method/property definition which used a `reference` or `reference_internal` return value policy would create a Python value that's bound by reference to a temporary C++ object, resulting in undefined behavior. For reasons that I was not able to figure out fully, it seems like this causes problems when using old versions of `boost::optional`, but not with recent versions of `boost::optional` or the `libstdc++` implementation of `std::optional`. The issue (that the override to `return_value_policy::move` is never being applied) is present for all implementations, it just seems like that somehow doesn't result in problems for the some implementation of `optional`. This change includes a regression type with a custom optional-like type which was able to reproduce the issue. Part of the issue with using the wrong types may have stemmed from the type variables `T` and `T_` having very similar names. This also changes the type variables in `optional_caster` to use slightly more descriptive names, which also more closely follow the naming convention used by the other STL casters. Fixes pybind#3330
1 parent ba9f919 commit 362f69e

File tree

3 files changed

+257
-10
lines changed

3 files changed

+257
-10
lines changed

include/pybind11/stl.h

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -245,17 +245,17 @@ template <typename Key, typename Value, typename Hash, typename Equal, typename
245245
: map_caster<std::unordered_map<Key, Value, Hash, Equal, Alloc>, Key, Value> { };
246246

247247
// This type caster is intended to be used for std::optional and std::experimental::optional
248-
template<typename T> struct optional_caster {
249-
using value_conv = make_caster<typename T::value_type>;
248+
template<typename Type, typename Value = typename Type::value_type> struct optional_caster {
249+
using value_conv = make_caster<Value>;
250250

251-
template <typename T_>
252-
static handle cast(T_ &&src, return_value_policy policy, handle parent) {
251+
template <typename T>
252+
static handle cast(T &&src, return_value_policy policy, handle parent) {
253253
if (!src)
254254
return none().inc_ref();
255255
if (!std::is_lvalue_reference<T>::value) {
256-
policy = return_value_policy_override<T>::policy(policy);
256+
policy = return_value_policy_override<Value>::policy(policy);
257257
}
258-
return value_conv::cast(*std::forward<T_>(src), policy, parent);
258+
return value_conv::cast(*std::forward<T>(src), policy, parent);
259259
}
260260

261261
bool load(handle src, bool convert) {
@@ -269,11 +269,11 @@ template<typename T> struct optional_caster {
269269
if (!inner_caster.load(src, convert))
270270
return false;
271271

272-
value.emplace(cast_op<typename T::value_type &&>(std::move(inner_caster)));
272+
value.emplace(cast_op<Value &&>(std::move(inner_caster)));
273273
return true;
274274
}
275275

276-
PYBIND11_TYPE_CASTER(T, _("Optional[") + value_conv::name + _("]"));
276+
PYBIND11_TYPE_CASTER(Type, _("Optional[") + value_conv::name + _("]"));
277277
};
278278

279279
#if defined(PYBIND11_HAS_OPTIONAL)

tests/test_stl.cpp

Lines changed: 182 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@
1919
#include <vector>
2020
#include <string>
2121

22+
#if defined(PYBIND11_TEST_BOOST)
23+
#include <boost/optional.hpp>
24+
25+
namespace pybind11 { namespace detail {
26+
template <typename T>
27+
struct type_caster<boost::optional<T>> : optional_caster<boost::optional<T>> {};
28+
29+
template <>
30+
struct type_caster<boost::none_t> : void_caster<boost::none_t> {};
31+
}} // namespace pybind11::detail
32+
#endif
33+
2234
// Test with `std::variant` in C++17 mode, or with `boost::variant` in C++11/14
2335
#if defined(PYBIND11_HAS_VARIANT)
2436
using std::variant;
@@ -67,6 +79,92 @@ struct OptionalHolder
6779
};
6880

6981

82+
enum class EnumType {
83+
k0 = 0,
84+
k1 = 1,
85+
};
86+
87+
// This is used to test that return-by-ref and return-by-copy policies are
88+
// handled properly for optional types. This is a regression test for a dangling
89+
// reference issue. The issue seemed to require the enum value type to
90+
// reproduce - it didn't seem to happen if the value type is just an integer.
91+
template <template <typename> class OptionalImpl>
92+
class OptionalProperties {
93+
public:
94+
using OptionalEnumValue = OptionalImpl<EnumType>;
95+
96+
OptionalProperties() : value(EnumType::k1) {}
97+
~OptionalProperties() {
98+
// Reset value to detect use-after-destruction.
99+
// This is set to a specific value rather than NULL to ensure that
100+
// the memory that contains the value gets re-written.
101+
value = EnumType::k0;
102+
}
103+
104+
OptionalEnumValue& access_by_ref() { return value; }
105+
OptionalEnumValue access_by_copy() { return value; }
106+
107+
private:
108+
OptionalEnumValue value;
109+
};
110+
111+
// This type mimics aspects of boost::optional from old versions of Boost,
112+
// which exposed a dangling reference bug in Pybind11. Recent versions of
113+
// boost::optional, as well as libstdc++'s std::optional, don't seem to be
114+
// affected by the same issue. This is meant to be a minimal implementation
115+
// required to reproduce the issue, not fully standard-compliant.
116+
// See issue #3330 for more details.
117+
template <typename T>
118+
class ReferenceSensitiveOptional {
119+
public:
120+
using value_type = T;
121+
122+
ReferenceSensitiveOptional() = default;
123+
ReferenceSensitiveOptional(const T& value) : storage{value} {}
124+
ReferenceSensitiveOptional(T&& value) : storage{std::move(value)} {}
125+
ReferenceSensitiveOptional& operator=(const T& value) {
126+
storage = {value};
127+
return *this;
128+
}
129+
ReferenceSensitiveOptional& operator=(T&& value) {
130+
storage = {std::move(value)};
131+
return *this;
132+
}
133+
134+
template <typename... Args>
135+
T& emplace(Args&&... args) {
136+
storage.clear();
137+
storage.emplace_back(std::forward<Args>(args)...);
138+
return storage.back();
139+
}
140+
141+
const T& value() const noexcept {
142+
assert(!storage.empty());
143+
return storage[0];
144+
}
145+
146+
const T& operator*() const noexcept {
147+
return value();
148+
}
149+
150+
const T* operator->() const noexcept {
151+
return &value();
152+
}
153+
154+
explicit operator bool() const noexcept {
155+
return !storage.empty();
156+
}
157+
158+
private:
159+
std::vector<T> storage;
160+
};
161+
162+
namespace pybind11 { namespace detail {
163+
template <typename T>
164+
struct type_caster<ReferenceSensitiveOptional<T>> : optional_caster<ReferenceSensitiveOptional<T>> {};
165+
}} // namespace pybind11::detail
166+
167+
70168
TEST_SUBMODULE(stl, m) {
71169
// test_vector
72170
m.def("cast_vector", []() { return std::vector<int>{1}; });
@@ -145,6 +243,10 @@ TEST_SUBMODULE(stl, m) {
145243
return v;
146244
});
147245

246+
pybind11::enum_<EnumType>(m, "EnumType")
247+
.value("k0", EnumType::k0)
248+
.value("k1", EnumType::k1);
249+
148250
// test_move_out_container
149251
struct MoveOutContainer {
150252
struct Value { int value; };
@@ -213,6 +315,12 @@ TEST_SUBMODULE(stl, m) {
213315
.def(py::init<>())
214316
.def_readonly("member", &opt_holder::member)
215317
.def("member_initialized", &opt_holder::member_initialized);
318+
319+
using opt_props = OptionalProperties<std::optional>;
320+
pybind11::class_<opt_props>(m, "OptionalProperties")
321+
.def(pybind11::init<>())
322+
.def_property_readonly("access_by_ref", &opt_props::access_by_ref)
323+
.def_property_readonly("access_by_copy", &opt_props::access_by_copy);
216324
#endif
217325

218326
#ifdef PYBIND11_HAS_EXP_OPTIONAL
@@ -239,8 +347,76 @@ TEST_SUBMODULE(stl, m) {
239347
.def(py::init<>())
240348
.def_readonly("member", &opt_exp_holder::member)
241349
.def("member_initialized", &opt_exp_holder::member_initialized);
350+
351+
using opt_exp_props = OptionalProperties<std::experimental::optional>;
352+
pybind11::class_<opt_exp_props>(m, "OptionalExpProperties")
353+
.def(pybind11::init<>())
354+
.def_property_readonly("access_by_ref", &opt_exp_props::access_by_ref)
355+
.def_property_readonly("access_by_copy", &opt_exp_props::access_by_copy);
356+
#endif
357+
358+
#if defined(PYBIND11_TEST_BOOST)
359+
// test_boost_optional
360+
m.attr("has_boost_optional") = true;
361+
362+
using boost_opt_int = boost::optional<int>;
363+
using boost_opt_no_assign = boost::optional<NoAssign>;
364+
m.def("double_or_zero_boost", [](const boost_opt_int& x) -> int {
365+
return x.value_or(0) * 2;
366+
});
367+
m.def("half_or_none_boost", [](int x) -> boost_opt_int {
368+
return x ? boost_opt_int(x / 2) : boost_opt_int();
369+
});
370+
m.def("test_nullopt_boost", [](boost_opt_int x) {
371+
return x.value_or(42);
372+
}, py::arg_v("x", boost::none, "None"));
373+
m.def("test_no_assign_boost", [](const boost_opt_no_assign &x) {
374+
return x ? x->value : 42;
375+
}, py::arg_v("x", boost::none, "None"));
376+
377+
using opt_boost_holder = OptionalHolder<boost::optional, MoveOutDetector>;
378+
py::class_<opt_boost_holder>(m, "OptionalBoostHolder", "Class with optional member")
379+
.def(py::init<>())
380+
.def_readonly("member", &opt_boost_holder::member)
381+
.def("member_initialized", &opt_boost_holder::member_initialized);
382+
383+
using opt_boost_props = OptionalProperties<boost::optional>;
384+
pybind11::class_<opt_boost_props>(m, "OptionalBoostProperties")
385+
.def(pybind11::init<>())
386+
.def_property_readonly("access_by_ref", &opt_boost_props::access_by_ref)
387+
.def_property_readonly("access_by_copy", &opt_boost_props::access_by_copy);
242388
#endif
243389

390+
// test_refsensitive_optional
391+
m.attr("has_refsensitive_optional") = true;
392+
393+
using refsensitive_opt_int = ReferenceSensitiveOptional<int>;
394+
using refsensitive_opt_no_assign = ReferenceSensitiveOptional<NoAssign>;
395+
m.def("double_or_zero_refsensitive", [](const refsensitive_opt_int& x) -> int {
396+
return (x ? x.value() : 0) * 2;
397+
});
398+
m.def("half_or_none_refsensitive", [](int x) -> refsensitive_opt_int {
399+
return x ? refsensitive_opt_int(x / 2) : refsensitive_opt_int();
400+
});
401+
m.def("test_nullopt_refsensitive", [](refsensitive_opt_int x) {
402+
return x ? x.value() : 42;
403+
}, py::arg_v("x", refsensitive_opt_int(), "None"));
404+
m.def("test_no_assign_refsensitive", [](const refsensitive_opt_no_assign &x) {
405+
return x ? x->value : 42;
406+
}, py::arg_v("x", refsensitive_opt_no_assign(), "None"));
407+
408+
using opt_refsensitive_holder = OptionalHolder<ReferenceSensitiveOptional, MoveOutDetector>;
409+
py::class_<opt_refsensitive_holder>(m, "OptionalRefSensitiveHolder", "Class with optional member")
410+
.def(py::init<>())
411+
.def_readonly("member", &opt_refsensitive_holder::member)
412+
.def("member_initialized", &opt_refsensitive_holder::member_initialized);
413+
414+
using opt_refsensitive_props = OptionalProperties<ReferenceSensitiveOptional>;
415+
pybind11::class_<opt_refsensitive_props>(m, "OptionalRefSensitiveProperties")
416+
.def(pybind11::init<>())
417+
.def_property_readonly("access_by_ref", &opt_refsensitive_props::access_by_ref)
418+
.def_property_readonly("access_by_copy", &opt_refsensitive_props::access_by_copy);
419+
244420
#ifdef PYBIND11_HAS_FILESYSTEM
245421
// test_fs_path
246422
m.attr("has_filesystem") = true;
@@ -280,8 +456,12 @@ TEST_SUBMODULE(stl, m) {
280456
m.def("tpl_ctor_set", [](std::unordered_set<TplCtorClass> &) {});
281457
#if defined(PYBIND11_HAS_OPTIONAL)
282458
m.def("tpl_constr_optional", [](std::optional<TplCtorClass> &) {});
283-
#elif defined(PYBIND11_HAS_EXP_OPTIONAL)
284-
m.def("tpl_constr_optional", [](std::experimental::optional<TplCtorClass> &) {});
459+
#endif
460+
#if defined(PYBIND11_HAS_EXP_OPTIONAL)
461+
m.def("tpl_constr_optional_exp", [](std::experimental::optional<TplCtorClass> &) {});
462+
#endif
463+
#if defined(PYBIND11_TEST_BOOST)
464+
m.def("tpl_constr_optional_boost", [](boost::optional<TplCtorClass> &) {});
285465
#endif
286466

287467
// test_vec_of_reference_wrapper

tests/test_stl.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ def test_optional():
132132
assert mvalue.initialized
133133
assert holder.member_initialized()
134134

135+
props = m.OptionalProperties()
136+
assert int(props.access_by_ref) == 1
137+
assert int(props.access_by_copy) == 1
138+
135139

136140
@pytest.mark.skipif(
137141
not hasattr(m, "has_exp_optional"), reason="no <experimental/optional>"
@@ -160,6 +164,69 @@ def test_exp_optional():
160164
assert mvalue.initialized
161165
assert holder.member_initialized()
162166

167+
props = m.OptionalExpProperties()
168+
assert int(props.access_by_ref) == 1
169+
assert int(props.access_by_copy) == 1
170+
171+
172+
@pytest.mark.skipif(not hasattr(m, "has_boost_optional"), reason="no <boost/optional>")
173+
def test_boost_optional():
174+
assert m.double_or_zero_boost(None) == 0
175+
assert m.double_or_zero_boost(42) == 84
176+
pytest.raises(TypeError, m.double_or_zero_boost, "foo")
177+
178+
assert m.half_or_none_boost(0) is None
179+
assert m.half_or_none_boost(42) == 21
180+
pytest.raises(TypeError, m.half_or_none_boost, "foo")
181+
182+
assert m.test_nullopt_boost() == 42
183+
assert m.test_nullopt_boost(None) == 42
184+
assert m.test_nullopt_boost(42) == 42
185+
assert m.test_nullopt_boost(43) == 43
186+
187+
assert m.test_no_assign_boost() == 42
188+
assert m.test_no_assign_boost(None) == 42
189+
assert m.test_no_assign_boost(m.NoAssign(43)) == 43
190+
pytest.raises(TypeError, m.test_no_assign_boost, 43)
191+
192+
holder = m.OptionalBoostHolder()
193+
mvalue = holder.member
194+
assert mvalue.initialized
195+
assert holder.member_initialized()
196+
197+
props = m.OptionalBoostProperties()
198+
assert int(props.access_by_ref) == 1
199+
assert int(props.access_by_copy) == 1
200+
201+
202+
def test_reference_sensitive_optional():
203+
assert m.double_or_zero_refsensitive(None) == 0
204+
assert m.double_or_zero_refsensitive(42) == 84
205+
pytest.raises(TypeError, m.double_or_zero_refsensitive, "foo")
206+
207+
assert m.half_or_none_refsensitive(0) is None
208+
assert m.half_or_none_refsensitive(42) == 21
209+
pytest.raises(TypeError, m.half_or_none_refsensitive, "foo")
210+
211+
assert m.test_nullopt_refsensitive() == 42
212+
assert m.test_nullopt_refsensitive(None) == 42
213+
assert m.test_nullopt_refsensitive(42) == 42
214+
assert m.test_nullopt_refsensitive(43) == 43
215+
216+
assert m.test_no_assign_refsensitive() == 42
217+
assert m.test_no_assign_refsensitive(None) == 42
218+
assert m.test_no_assign_refsensitive(m.NoAssign(43)) == 43
219+
pytest.raises(TypeError, m.test_no_assign_refsensitive, 43)
220+
221+
holder = m.OptionalRefSensitiveHolder()
222+
mvalue = holder.member
223+
assert mvalue.initialized
224+
assert holder.member_initialized()
225+
226+
props = m.OptionalRefSensitiveProperties()
227+
assert int(props.access_by_ref) == 1
228+
assert int(props.access_by_copy) == 1
229+
163230

164231
@pytest.mark.skipif(not hasattr(m, "has_filesystem"), reason="no <filesystem>")
165232
def test_fs_path():

0 commit comments

Comments
 (0)