Skip to content

Commit 87c1fa1

Browse files
committed
Add tests for py::cast() calls which create temporaries
1 parent a880157 commit 87c1fa1

File tree

3 files changed

+47
-5
lines changed

3 files changed

+47
-5
lines changed

include/pybind11/cast.h

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,14 @@ class loader_life_support {
135135
stack.shrink_to_fit();
136136
}
137137

138-
/// This can only be used by casters invoked by `argument_loader`.
139-
/// If `py::cast()` tries to use it, an exception is thrown because
140-
/// the temporary object's life can't be extended.
138+
/// This can only be used inside a pybind11-bound function, either by `argument_loader`
139+
/// at argument preparation time or by `py::cast()` at execution time.
141140
PYBIND11_NOINLINE static void add_patient(handle h) {
142141
auto &stack = get_internals().loader_patient_stack;
143142
if (stack.empty())
144-
pybind11_fail("`py::cast()` cannot perform Python -> C++ conversions "
145-
"which require the creation of temporary values");
143+
throw cast_error("When called outside a bound function, py::cast() cannot "
144+
"do Python -> C++ conversions which require the creation "
145+
"of temporary values");
146146

147147
auto result = PyList_Append(stack.back(), h.ptr());
148148
if (result == -1)

tests/test_class.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,40 @@ TEST_SUBMODULE(class_, m) {
150150
py::class_<MyDerived, MyBase>(m, "MyDerived")
151151
.def_static("make", &MyDerived::make)
152152
.def_static("make2", &MyDerived::make);
153+
154+
// test_implicit_conversion_life_support
155+
struct ConvertibleFromUserType {
156+
int i;
157+
158+
ConvertibleFromUserType(UserType u) : i(u.value()) { }
159+
};
160+
161+
py::class_<ConvertibleFromUserType>(m, "AcceptsUserType")
162+
.def(py::init<UserType>());
163+
py::implicitly_convertible<UserType, ConvertibleFromUserType>();
164+
165+
m.def("implicitly_convert_argument", [](const ConvertibleFromUserType &r) { return r.i; });
166+
m.def("implicitly_convert_variable", [](py::object o) {
167+
// `o` is `UserType` and `r` is a reference to a temporary created by implicit
168+
// conversion. This is valid when called inside a bound function because the temp
169+
// object is attached to the same life support system as the arguments.
170+
auto r = o.cast<const ConvertibleFromUserType &>();
171+
return r.i;
172+
});
173+
m.add_object("implicitly_convert_variable_fail", [&] {
174+
auto f = [](PyObject *, PyObject *args) -> PyObject * {
175+
auto o = py::reinterpret_borrow<py::tuple>(args)[0];
176+
try { // It should fail here because there is no life support.
177+
o.cast<const ConvertibleFromUserType &>();
178+
} catch (const py::cast_error &e) {
179+
return py::str(e.what()).release().ptr();
180+
}
181+
return py::str().release().ptr();
182+
};
183+
184+
auto def = new PyMethodDef{"f", f, METH_VARARGS, nullptr};
185+
return py::reinterpret_steal<py::object>(PyCFunction_NewEx(def, nullptr, m.ptr()));
186+
}());
153187
}
154188

155189
template <int N> class BreaksBase {};

tests/test_class.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,11 @@ def test_override_static():
119119
assert isinstance(b, m.MyBase)
120120
assert isinstance(d1, m.MyDerived)
121121
assert isinstance(d2, m.MyDerived)
122+
123+
124+
def test_implicit_conversion_life_support():
125+
"""Ensure the lifetime of temporary objects created for implicit conversions"""
126+
assert m.implicitly_convert_argument(UserType(5)) == 5
127+
assert m.implicitly_convert_variable(UserType(5)) == 5
128+
129+
assert "outside a bound function" in m.implicitly_convert_variable_fail(UserType(5))

0 commit comments

Comments
 (0)