Skip to content

Commit 1220156

Browse files
committed
Be more careful with move semantics in type casters
This commit addresses a serious issue involving combinations of bound types (e.g., ``T``) and type casters (e.g., ``std::vector<T>``), where nanobind was too aggressive in its use of move semantics. Calling a bound function from Python taking such a list (e.g., ``f([t1, t2, ..])``) would lead to the destruction of the Python objects ``t1, t2, ..`` if the type ``T`` exposed a move constructor, which is highly non-intuitive. This commit fixes, simplifies, and documents the rules of this process. In particular, ``type_caster::Cast`` continues to serve its role to infer what types a type caster can and wants to produce. It aggressively tries to move, but now only does so when this is legal (i.e., when the type caster created a temporary copy that is safe to move without affecting program state elsewhere). This involves two new default casting rules: ``movable_cast_t`` (adapted to be more aggressive with moves) and ``precise_cast_t`` for bound types that does not involve a temporary and consequently should only move when this was explicitly requested by the user. The fix also revealed inefficiencies in the previous implementation where moves were actually possible but not done (e.g. for functions taking an STL list by value). Some binding projects may see speedups as a consequence of this change. Fixes issue wjakob#307.
1 parent 70abddc commit 1220156

22 files changed

+205
-228
lines changed

include/nanobind/eigen/dense.h

-3
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,6 @@ struct type_caster<T, enable_if_t<is_eigen_xpr_v<T> &&
218218
using Array = Eigen::Array<typename T::Scalar, T::RowsAtCompileTime,
219219
T::ColsAtCompileTime>;
220220
using Caster = make_caster<Array>;
221-
static constexpr bool IsClass = false;
222221
static constexpr auto Name = Caster::Name;
223222
template <typename T_> using Cast = T;
224223

@@ -249,7 +248,6 @@ struct type_caster<Eigen::Map<T, Options, StrideType>,
249248
const typename Map::Scalar,
250249
typename Map::Scalar>>;
251250
using NDArrayCaster = type_caster<NDArray>;
252-
static constexpr bool IsClass = false;
253251
static constexpr auto Name = NDArrayCaster::Name;
254252
template <typename T_> using Cast = Map;
255253

@@ -395,7 +393,6 @@ struct type_caster<Eigen::Ref<T, Options, StrideType>,
395393
static constexpr bool DMapConstructorOwnsData =
396394
!Eigen::internal::traits<Ref>::template match<DMap>::type::value;
397395

398-
static constexpr bool IsClass = false;
399396
static constexpr auto Name =
400397
const_name<MaybeConvert>(DMapCaster::Name, MapCaster::Name);
401398

include/nanobind/nb_cast.h

+46-25
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,16 @@
99

1010
#define NB_TYPE_CASTER(Value_, descr) \
1111
using Value = Value_; \
12-
static constexpr bool IsClass = false; \
1312
static constexpr auto Name = descr; \
1413
template <typename T_> using Cast = movable_cast_t<T_>; \
1514
static handle from_cpp(Value *p, rv_policy policy, cleanup_list *list) { \
1615
if (!p) \
1716
return none().release(); \
1817
return from_cpp(*p, policy, list); \
1918
} \
20-
explicit operator Value *() { return &value; } \
21-
explicit operator Value &() { return value; } \
22-
explicit operator Value &&() && { return (Value &&) value; } \
19+
explicit operator Value*() { return &value; } \
20+
explicit operator Value&() { return (Value &) value; } \
21+
explicit operator Value&&() { return (Value &&) value; } \
2322
Value value;
2423

2524
#define NB_MAKE_OPAQUE(...) \
@@ -38,14 +37,45 @@ enum cast_flags : uint8_t {
3837
construct = (1 << 1)
3938
};
4039

40+
/**
41+
* Type casters expose a member 'Cast<T>' which users of a type caster must
42+
* query to determine what the caster actually can (and prefers) to produce.
43+
* The convenience alias ``cast_t<T>`` defined below performs this query for a
44+
* given type ``T``.
45+
*
46+
* Often ``cast_t<T>`` is simply equal to ``T`` or ``T&``. More significant
47+
* deviations are also possible, which could be due to one of the following
48+
* two reasons:
49+
*
50+
* 1. Efficiency: most STL type casters create a local copy (``value`` member)
51+
* of the value being cast. The caller should move this value to its
52+
* intended destination instead of making further copies along the way.
53+
* Consequently, ``cast_t<std::vector<T>>`` yields ``cast_t<std::vector<T>>
54+
* &&`` to enable such behavior.
55+
*
56+
* 2. STL pairs may contain references, and such pairs aren't
57+
* default-constructible. The STL pair caster therefore cannot create a local
58+
* copy and must construct the pair on the fly, which in turns means that it
59+
* cannot return references. Therefore, ``cast_t<const std::pair<T1, T2>&>``
60+
* yields ``std::pair<T1, T2>``.
61+
*/
62+
63+
/// Ask a type caster what flavors of a type it can actually produce -- may be different from 'T'
4164
template <typename T> using cast_t = typename make_caster<T>::template Cast<T>;
4265

66+
/// This is a default choice for the 'Cast' type alias described above. It
67+
/// prefers to return rvalue references to allow the caller to move the object.
4368
template <typename T>
44-
using simple_cast_t =
45-
std::conditional_t<is_pointer_v<T>, intrinsic_t<T> *, intrinsic_t<T> &>;
69+
using movable_cast_t =
70+
std::conditional_t<is_pointer_v<T>, intrinsic_t<T> *,
71+
std::conditional_t<std::is_lvalue_reference_v<T>,
72+
intrinsic_t<T> &, intrinsic_t<T> &&>>;
4673

74+
/// This version is more careful about what the caller actually requested and
75+
/// only moves when this was explicitly requested. It is the default for the
76+
/// base type caster (i.e., types bound via ``nanobind::class_<..>``)
4777
template <typename T>
48-
using movable_cast_t =
78+
using precise_cast_t =
4979
std::conditional_t<is_pointer_v<T>, intrinsic_t<T> *,
5080
std::conditional_t<std::is_rvalue_reference_v<T>,
5181
intrinsic_t<T> &&, intrinsic_t<T> &>>;
@@ -105,13 +135,11 @@ struct type_caster<T, enable_if_t<std::is_arithmetic_v<T> && !is_std_char_v<T>>>
105135

106136
template <> struct type_caster<void_type> {
107137
static constexpr auto Name = const_name("None");
108-
static constexpr bool IsClass = false;
109138
};
110139

111140
template <> struct type_caster<void> {
112141
template <typename T_> using Cast = void *;
113142
using Value = void*;
114-
static constexpr bool IsClass = false;
115143
static constexpr auto Name = const_name("capsule");
116144
explicit operator void *() { return value; }
117145
Value value;
@@ -175,7 +203,6 @@ template <> struct type_caster<bool> {
175203
template <> struct type_caster<char> {
176204
using Value = const char *;
177205
Value value;
178-
static constexpr bool IsClass = false;
179206
static constexpr auto Name = const_name("str");
180207
template <typename T_>
181208
using Cast = std::conditional_t<is_pointer_v<T_>, const char *, char>;
@@ -289,12 +316,10 @@ template <typename T> NB_INLINE rv_policy infer_policy(rv_policy policy) {
289316

290317
template <typename T, typename SFINAE = int> struct type_hook : std::false_type { };
291318

292-
template <typename Type_> struct type_caster_base {
319+
template <typename Type_> struct type_caster_base : type_caster_base_tag {
293320
using Type = Type_;
294321
static constexpr auto Name = const_name<Type>();
295-
static constexpr bool IsClass = true;
296-
297-
template <typename T> using Cast = movable_cast_t<T>;
322+
template <typename T> using Cast = precise_cast_t<T>;
298323

299324
NB_INLINE bool from_python(handle src, uint8_t flags,
300325
cleanup_list *cleanup) noexcept {
@@ -335,7 +360,7 @@ template <typename Type_> struct type_caster_base {
335360
return *value;
336361
}
337362

338-
operator Type&&() && {
363+
operator Type&&() {
339364
raise_next_overload_if_null(value);
340365
return (Type &&) *value;
341366
}
@@ -352,7 +377,6 @@ NAMESPACE_END(detail)
352377
template <typename T, typename Derived>
353378
bool try_cast(const detail::api<Derived> &value, T &out, bool convert = true) noexcept {
354379
using Caster = detail::make_caster<T>;
355-
using Output = typename Caster::template Cast<T>;
356380

357381
static_assert(!std::is_same_v<const char *, T>,
358382
"nanobind::try_cast(): cannot return a reference to a temporary.");
@@ -361,11 +385,7 @@ bool try_cast(const detail::api<Derived> &value, T &out, bool convert = true) no
361385
if (caster.from_python(value.derived().ptr(),
362386
convert ? (uint8_t) detail::cast_flags::convert
363387
: (uint8_t) 0, nullptr)) {
364-
if constexpr (Caster::IsClass)
365-
out = caster.operator Output();
366-
else
367-
out = std::move(caster.operator Output&&());
368-
388+
out = caster.operator detail::cast_t<T>();
369389
return true;
370390
}
371391

@@ -378,11 +398,11 @@ T cast(const detail::api<Derived> &value, bool convert = true) {
378398
return;
379399
} else {
380400
using Caster = detail::make_caster<T>;
381-
using Output = typename Caster::template Cast<T>;
382401

383402
static_assert(
384-
!(std::is_reference_v<T> || std::is_pointer_v<T>) || Caster::IsClass ||
385-
std::is_same_v<const char *, T>,
403+
!(std::is_reference_v<T> || std::is_pointer_v<T>) ||
404+
detail::is_base_caster_v<Caster> ||
405+
std::is_same_v<const char *, T>,
386406
"nanobind::cast(): cannot return a reference to a temporary.");
387407

388408
Caster caster;
@@ -397,7 +417,7 @@ T cast(const detail::api<Derived> &value, bool convert = true) {
397417
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
398418
#endif
399419

400-
return caster.operator Output();
420+
return caster.operator detail::cast_t<T>();
401421

402422
#if defined(__GNUC__) && !defined(__clang__)
403423
#pragma GCC diagnostic pop
@@ -411,6 +431,7 @@ object cast(T &&value, rv_policy policy = rv_policy::automatic_reference) {
411431
policy, nullptr);
412432
if (!h.is_valid())
413433
detail::raise_cast_error();
434+
414435
return steal(h);
415436
}
416437

include/nanobind/nb_class.h

+22-18
Original file line numberDiff line numberDiff line change
@@ -293,12 +293,6 @@ template <typename... Args> struct init {
293293
NB_INLINE static void execute(Class &cl, const Extra&... extra) {
294294
using Type = typename Class::Type;
295295
using Alias = typename Class::Alias;
296-
static_assert(
297-
detail::make_caster<Type>::IsClass,
298-
"Attempted to create a constructor for a type that won't be "
299-
"handled by the nanobind's class type caster. Is it possible that "
300-
"you forgot to add NB_MAKE_OPAQUE() somewhere?");
301-
302296
cl.def(
303297
"__init__",
304298
[](pointer_and_handle<Type> v, Args... args) {
@@ -324,12 +318,6 @@ template <typename Arg> struct init_implicit {
324318
NB_INLINE static void execute(Class &cl, const Extra&... extra) {
325319
using Type = typename Class::Type;
326320
using Alias = typename Class::Alias;
327-
using Caster = detail::make_caster<Arg>;
328-
static_assert(
329-
detail::make_caster<Type>::IsClass,
330-
"Attempted to create a constructor for a type that won't be "
331-
"handled by the nanobind's class type caster. Is it possible that "
332-
"you forgot to add NB_MAKE_OPAQUE() somewhere?");
333321

334322
cl.def(
335323
"__init__",
@@ -344,7 +332,9 @@ template <typename Arg> struct init_implicit {
344332
new ((Alias *) v.p) Alias{ (detail::forward_t<Arg>) arg };
345333
}, is_implicit(), extra...);
346334

347-
if constexpr (!Caster::IsClass) {
335+
using Caster = detail::make_caster<Arg>;
336+
337+
if constexpr (!detail::is_class_caster_v<Caster>) {
348338
detail::implicitly_convertible(
349339
[](PyTypeObject *, PyObject *src,
350340
detail::cleanup_list *cleanup) noexcept -> bool {
@@ -364,12 +354,22 @@ class class_ : public object {
364354
using Base = typename detail::extract<T, detail::is_base, Ts...>::type;
365355
using Alias = typename detail::extract<T, detail::is_alias, Ts...>::type;
366356

367-
static_assert(sizeof(Alias) < (1 << 24), "instance size is too big!");
368-
static_assert(alignof(Alias) < (1 << 8), "instance alignment is too big!");
357+
static_assert(sizeof(Alias) < (1 << 24), "Instance size is too big!");
358+
static_assert(alignof(Alias) < (1 << 8), "Instance alignment is too big!");
369359
static_assert(
370360
sizeof...(Ts) == !std::is_same_v<Base, T> + !std::is_same_v<Alias, T>,
371361
"nanobind::class_<> was invoked with extra arguments that could not be handled");
372362

363+
static_assert(
364+
detail::is_base_caster_v<detail::make_caster<Type>>,
365+
"You attempted to bind a type that is already intercepted by a type "
366+
"caster. Having both at the same time is not allowed. Are you perhaps "
367+
"binding an STL type, while at the same time including a matching "
368+
"type caster from <nanobind/stl/*>? Or did you perhaps forget to "
369+
"declare NB_MAKE_OPAQUE(..) to specifically disable the type caster "
370+
"catch-all for a specific type? Please review the documentation "
371+
"to learn about the difference between bindings and type casters.");
372+
373373
template <typename... Extra>
374374
NB_INLINE class_(handle scope, const char *name, const Extra &... extra) {
375375
detail::type_init_data d;
@@ -521,7 +521,9 @@ class class_ : public object {
521521
static_assert(std::is_base_of_v<C, T>,
522522
"def_rw() requires a (base) class member!");
523523

524-
using Q = std::conditional_t<detail::make_caster<D>::IsClass, const D &, D &&>;
524+
using Q =
525+
std::conditional_t<detail::is_base_caster_v<detail::make_caster<D>>,
526+
const D &, D &&>;
525527

526528
def_prop_rw(name,
527529
[p](const T &c) -> const D & { return c.*p; },
@@ -534,7 +536,9 @@ class class_ : public object {
534536
template <typename D, typename... Extra>
535537
NB_INLINE class_ &def_rw_static(const char *name, D *p,
536538
const Extra &...extra) {
537-
using Q = std::conditional_t<detail::make_caster<D>::IsClass, const D &, D &&>;
539+
using Q =
540+
std::conditional_t<detail::is_base_caster_v<detail::make_caster<D>>,
541+
const D &, D &&>;
538542

539543
def_prop_rw_static(name,
540544
[p](handle) -> const D & { return *p; },
@@ -624,7 +628,7 @@ template <typename T> class enum_ : public class_<T> {
624628
template <typename Source, typename Target> void implicitly_convertible() {
625629
using Caster = detail::make_caster<Source>;
626630

627-
if constexpr (Caster::IsClass) {
631+
if constexpr (detail::is_base_caster_v<Caster>) {
628632
detail::implicitly_convertible(&typeid(Source), &typeid(Target));
629633
} else {
630634
detail::implicitly_convertible(

include/nanobind/nb_func.h

+2-2
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,12 @@ NB_INLINE PyObject *func_create(Func &&func, Return (*)(Args...),
121121

122122
PyObject *result;
123123
if constexpr (std::is_void_v<Return>) {
124-
cap->func(((make_caster<Args>&&) in.template get<Is>()).operator cast_t<Args>()...);
124+
cap->func(in.template get<Is>().operator cast_t<Args>()...);
125125
result = Py_None;
126126
Py_INCREF(result);
127127
} else {
128128
result = cast_out::from_cpp(
129-
cap->func(((make_caster<Args> &&) in.template get<Is>())
129+
cap->func((in.template get<Is>())
130130
.operator cast_t<Args>()...),
131131
policy, cleanup).ptr();
132132
}

include/nanobind/nb_traits.h

+15
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,21 @@ template <typename T>
142142
constexpr bool has_shared_from_this_v =
143143
decltype(has_shared_from_this_impl((T *) nullptr))::value;
144144

145+
/// Base of all type casters for traditional bindings created via nanobind::class_<>
146+
struct type_caster_base_tag {
147+
static constexpr bool IsClass = true;
148+
};
149+
150+
/// Check if a type caster represents traditional bindings created via nanobind::class_<>
151+
template <typename Caster>
152+
constexpr bool is_base_caster_v = std::is_base_of_v<type_caster_base_tag, Caster>;
153+
154+
template <typename T> using is_class_caster_test = std::enable_if_t<T::IsClass>;
155+
156+
/// Generalized version of the is_base_caster_v test that also accepts unique_ptr/shared_ptr
157+
template <typename Caster>
158+
constexpr bool is_class_caster_v = detail::detector<void, is_class_caster_test, Caster>::value;
159+
145160
NAMESPACE_END(detail)
146161

147162
template <typename... Args>

include/nanobind/nb_types.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ template <typename T>
551551
NB_INLINE bool isinstance(handle h) noexcept {
552552
if constexpr (std::is_base_of_v<handle, T>)
553553
return T::check_(h);
554-
else if constexpr (detail::make_caster<T>::IsClass)
554+
else if constexpr (detail::is_base_caster_v<detail::make_caster<T>>)
555555
return detail::nb_type_isinstance(h.ptr(), &typeid(detail::intrinsic_t<T>));
556556
else
557557
return detail::make_caster<T>().from_python(h, 0, nullptr);

include/nanobind/stl/detail/nb_array.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ template <typename Value_, typename Entry, size_t Size> struct array_caster {
2727
break;
2828
}
2929

30-
value[i] = ((Caster &&) caster).operator cast_t<Entry &&>();
30+
value[i] = caster.operator cast_t<Entry>();
3131
}
3232

3333
Py_XDECREF(temp);

include/nanobind/stl/detail/nb_dict.h

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ template <typename Value_, typename Key, typename Element> struct dict_caster {
5151
break;
5252
}
5353

54-
value.emplace(((KeyCaster &&) key_caster).operator cast_t<Key &&>(),
55-
((ElementCaster &&) element_caster).operator cast_t<Element &&>());
54+
value.emplace(key_caster.operator cast_t<Key>(),
55+
element_caster.operator cast_t<Element>());
5656
}
5757

5858
Py_DECREF(items);

include/nanobind/stl/detail/nb_list.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ template <typename Value_, typename Entry> struct list_caster {
4444
break;
4545
}
4646

47-
value.push_back(((Caster &&) caster).operator cast_t<Entry &&>());
47+
value.push_back(caster.operator cast_t<Entry>());
4848
}
4949

5050
Py_XDECREF(temp);

include/nanobind/stl/detail/nb_set.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ template <typename Value_, typename Key> struct set_caster {
3939
if (!success)
4040
break;
4141

42-
value.emplace(((KeyCaster &&) key_caster).operator cast_t<Key&&>());
42+
value.emplace(key_caster.operator cast_t<Key>());
4343
}
4444

4545
if (PyErr_Occurred()) {

include/nanobind/stl/detail/traits.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ template <typename T>
6262
constexpr bool is_copy_assignable_v = is_copy_assignable<T>::value;
6363

6464
// Analogous template for checking comparability
65-
template<typename T> using comparable_test = decltype(std::declval<T>() == std::declval<T>());
65+
template <typename T> using comparable_test = decltype(std::declval<T>() == std::declval<T>());
6666

6767
template <typename T, typename SFINAE = int>
6868
struct is_equality_comparable {

0 commit comments

Comments
 (0)