Skip to content

Commit 342677a

Browse files
committed
Support nb::init(<lambda>) as syntactic sugar for custom constructors
1 parent 534fd8c commit 342677a

File tree

8 files changed

+292
-53
lines changed

8 files changed

+292
-53
lines changed

docs/api_core.rst

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2191,9 +2191,14 @@ Class binding
21912191
21922192
.. cpp:function:: template <typename... Args, typename... Extra> class_ &def(init<Args...> arg, const Extra &... extra)
21932193

2194-
Bind a constructor. The variable length `extra` parameter can be used to
2194+
Bind a C++ constructor that takes parameters of types ``Args...``.
2195+
The variable length `extra` parameter can be used to
21952196
pass a docstring and other :ref:`function binding annotations
2196-
<function_binding_annotations>`.
2197+
<function_binding_annotations>`. You can also bind a custom constructor
2198+
(one that does not exist in the C++ code) by writing
2199+
``.def(nb::init(<lambda>))``, provided the lambda returns an instance of
2200+
the class by value. If you need to wrap a factory function that returns
2201+
a pointer or shared pointer, see :cpp:struct:`nb::new_() <new_>` instead.
21972202

21982203
.. cpp:function:: template <typename Arg, typename... Extra> class_ &def(init_implicit<Arg> arg, const Extra &... extra)
21992204

@@ -2560,62 +2565,96 @@ Class binding
25602565
constructor. It is only meant to be used in binding declarations done via
25612566
:cpp:func:`class_::def()`.
25622567

2563-
Sometimes, it is necessary to bind constructors that don't exist in the
2564-
underlying C++ type (meaning that they are specific to the Python bindings).
2565-
Because `init` only works for existing C++ constructors, this requires
2566-
a manual workaround noting that
2567-
2568-
.. code-block:: cpp
2569-
2570-
nb::class_<MyType>(m, "MyType")
2571-
.def(nb::init<const char*, int>());
2572-
2573-
is syntax sugar for the following lower-level implementation using
2574-
"`placement new <https://en.wikipedia.org/wiki/Placement_syntax>`_":
2568+
To bind a constructor that exists in the C++ class, taking ``Args...``, write
2569+
``nb::init<Args...>()``.
2570+
2571+
To bind a constructor that is specific to the Python bindings (a
2572+
"custom constructor"), write ``nb::init(<some function>)`` (write a
2573+
lambda expression or a function pointer inside the
2574+
parentheses). The function should return a prvalue of the bound
2575+
type, by ending with a statement like ``return MyType(some,
2576+
args);``. If you write a custom constructor in this way, then
2577+
nanobind can construct the object without any extra copies or
2578+
moves, and the object therefore doesn't need to be copyable or movable.
2579+
2580+
If your custom constructor needs to take some actions after constructing
2581+
the C++ object, then nanobind recommends that you eschew
2582+
:cpp:struct:`nb::init() <init>` and instead bind an ``__init__`` method
2583+
directly. By convention, any nanobind method named ``"__init__"`` will
2584+
receive as its first argument a pointer to uninitialized storage that it
2585+
can initialize using `placement new
2586+
<https://en.wikipedia.org/wiki/Placement_syntax>`_:
25752587

25762588
.. code-block:: cpp
25772589
25782590
nb::class_<MyType>(m, "MyType")
25792591
.def("__init__",
25802592
[](MyType* t, const char* arg0, int arg1) {
25812593
new (t) MyType(arg0, arg1);
2594+
t->doSomething();
25822595
});
25832596
25842597
The provided lambda function will be called with a pointer to uninitialized
25852598
memory that has already been allocated (this memory region is co-located
25862599
with the Python object for reasons of efficiency). The lambda function can
25872600
then either run an in-place constructor and return normally (in which case
25882601
the instance is assumed to be correctly constructed) or fail by raising an
2589-
exception.
2602+
exception. If an exception is raised, nanobind assumes the object *was not*
2603+
constructed; in the above example, if ``doSomething()`` could throw, then you
2604+
would need to take care to call the destructor explicitly (``t->~MyType();``)
2605+
in case of an exception after the C++ constructor had completed.
2606+
2607+
When binding a custom constructor using :cpp:struct:`nb::init() <init>` for
2608+
a type that supports :ref:`overriding virtual methods in Python
2609+
<trampolines>`, you must return either an instance of the trampoline
2610+
type (``PyPet`` in ``nb::class_<Pet, PyPet>(...)``) or something that
2611+
can initialize both the bound type and the trampoline type (e.g.,
2612+
you can return a ``Pet`` if there exists a ``PyPet(Pet&&)`` constructor).
2613+
If that's not possible, you can alternatively write :cpp:struct:`nb::init()
2614+
<init>` with two function arguments instead of one. The first returns
2615+
an instance of the bound type (``Pet``), and will be called when constructing
2616+
an instance of the C++ class that has not been extended from Python.
2617+
The second returns an instance of the trampoline type (``PyPet``),
2618+
and will be called when constructing an instance that does need to consider
2619+
the possibility of Python-based virtual method overrides.
2620+
2621+
.. note:: :cpp:struct:`nb::init() <init>` always creates Python ``__init__``
2622+
methods, which construct a C++ object in already-allocated Python object
2623+
storage. If you need to wrap a constructor that performs its own
2624+
allocation, such as a factory function that returns a pointer, you must
2625+
use :cpp:struct:`nb::new_() <new_>` instead in order to create a Python
2626+
``__new__`` method.
25902627

25912628
.. cpp:struct:: template <typename Arg> init_implicit
25922629

25932630
See :cpp:class:`init` for detail on binding constructors. The main
2594-
difference between :cpp:class:`init` and `init_implicit` is that the latter
2595-
only supports constructors taking a single argument `Arg`, and that it marks
2596-
the constructor as usable for implicit conversions from `Arg`.
2631+
difference between :cpp:class:`init` and `init_implicit` is that the latter
2632+
only supports constructors that exist in the C++ code and take a single
2633+
argument `Arg`, and that it marks the constructor as usable for implicit
2634+
conversions from `Arg`.
25972635

25982636
Sometimes, it is necessary to bind implicit conversion-capable constructors
25992637
that don't exist in the underlying C++ type (meaning that they are specific
2600-
to the Python bindings). This can be done manually noting that
2638+
to the Python bindings). This can be done manually, noting that
26012639

26022640
.. code-block:: cpp
26032641
2604-
nb::class_<MyType>(m, "MyType")
2605-
.def(nb::init_implicit<const char*>());
2642+
nb::class_<MyType>(m, "MyType")
2643+
.def(nb::init_implicit<const char*>());
26062644
26072645
can be replaced by the lower-level code
26082646

26092647
.. code-block:: cpp
26102648
26112649
nb::class_<MyType>(m, "MyType")
2612-
.def("__init__",
2613-
[](MyType* t, const char* arg0) {
2614-
new (t) MyType(arg0);
2615-
});
2650+
.def(nb::init<const char*>());
26162651
26172652
nb::implicitly_convertible<const char*, MyType>();
26182653
2654+
and that this transformation works equally well if you use one of the forms
2655+
of :cpp:class:`nb::init() <init>` that cannot be expressed by
2656+
:cpp:class:`init_implicit`.
2657+
26192658
.. cpp:struct:: template <typename Func> new_
26202659

26212660
This is a small helper class that indicates to :cpp:func:`class_::def()`

docs/changelog.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,17 @@ Version TBD (not yet released)
6565
not be an instance of the alias/trampoline type.
6666
(PR `#859 <https://github.com/wjakob/nanobind/pull/859>`__)
6767

68+
- :cpp:struct:`nb::init() <init>` may now be written with a function argument
69+
and no template parameters to express a custom constructor that doesn't exist
70+
in C++. For example, you could use this to adapt Python strings to a
71+
pointer-and-length argument convention:
72+
``.def(nb::init([](std::string_view sv) { return MyType(sv.data(), sv.size()); }))``.
73+
This feature is syntactic sugar for writing a custom ``"__init__"`` binding
74+
using placement new, which remains fully supported, and which should continue
75+
to be used in cases where the custom constructor cannot be written as a
76+
function that finishes by returning a prvalue (``return MyType(some, args);``).
77+
(PR `#885 <https://github.com/wjakob/nanobind/pull/885>`__)
78+
6879
Version 2.4.0 (Dec 6, 2024)
6980
---------------------------
7081

docs/classes.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,8 @@ propagated to Python:
543543
544544
To fix this behavior, you must implement a *trampoline class*. A trampoline has
545545
the sole purpose of capturing virtual function calls in C++ and forwarding them
546-
to Python.
546+
to Python. (If you're reading nanobind's source code, you might see references
547+
to an *alias class*; it's the same thing as a trampoline class.)
547548

548549
.. code-block:: cpp
549550

docs/porting.rst

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -146,30 +146,66 @@ accepts ``std::shared_ptr<T>``. That means a C++ function that accepts
146146
a raw ``T*`` and calls ``shared_from_this()`` on it might stop working
147147
when ported from pybind11 to nanobind. You can solve this problem
148148
by always passing such objects across the Python/C++ boundary as
149-
``std::shared_ptr<T>`` rather than as ``T*``. See the :ref:`advanced section
149+
``std::shared_ptr<T>`` rather than as ``T*``, or by exposing all
150+
constructors using :cpp:struct:`nb::new_() <new_>` wrappers that
151+
return ``std::shared_ptr<T>``. See the :ref:`advanced section
150152
on object ownership <enable_shared_from_this>` for more details.
151153

152154
Custom constructors
153155
-------------------
154156
In pybind11, custom constructors (i.e. ones that do not already exist in the
155157
C++ class) could be specified as a lambda function returning an instance of
156-
the desired type.
158+
the desired type or a pointer to it.
157159

158160
.. code-block:: cpp
159161
160162
py::class_<MyType>(m, "MyType")
161-
.def(py::init([](int) { return MyType(...); }));
163+
.def(py::init([](int) { return MyType(...); }))
164+
.def(py::init([](std::string_view) {
165+
return std::make_unique<MyType>(...);
166+
}));
167+
168+
nanobind supports only the first form (where the lambda returns by value). Note
169+
that thanks to C++17's guaranteed copy elision, it now works even for types that
170+
are not copyable or movable, so you may be able to mechanically convert custom
171+
constructors that return by pointer into those that return by value.
172+
173+
.. note:: If *any* of your custom constructors still need to return a pointer or
174+
smart pointer, perhaps because they wrap a C++ factory method that only
175+
exposes those return types, you must switch *all* of them to use
176+
:cpp:struct:`nb::new_() <new_>` instead of :cpp:struct:`nb::init() <init>`.
177+
Be aware that :cpp:struct:`nb::new_() <new_>` cannot construct in-place, so
178+
using it gives up some of nanobind's performance benefits (but should still be
179+
faster than ``py::init()`` in pybind11). It comes with some other caveats
180+
as well, which are explained in the documentation on :ref:`customizing
181+
Python object creation <custom_new>`.
182+
183+
Guaranteed copy elision only works if the object is constructed as a temporary
184+
directly within the ``return`` statement. If you need to do something to the
185+
object before you return it, as in this example:
162186

163-
Unfortunately, the implementation of this feature was quite complex and
164-
often required further internal calls to the move or copy
165-
constructor. nanobind instead reverts to how pybind11 originally
166-
implemented this feature using in-place construction (`placement
167-
new <https://en.wikipedia.org/wiki/Placement_syntax>`_):
187+
.. code-block:: cpp
188+
189+
py::class_<MyType>(m, "MyType")
190+
.def(py::init([](int value) {
191+
auto ret = MyType();
192+
ret.value = value;
193+
return ret;
194+
}));
195+
196+
then ``MyType`` must be movable, and depending on compiler optimizations the move
197+
constructor might actually be called at runtime, which is more expensive than
198+
in-place construction. In such cases, nanobind recommends instead that you
199+
directly bind a ``__init__`` method using `placement new
200+
<https://en.wikipedia.org/wiki/Placement_syntax>`_:
168201

169202
.. code-block:: cpp
170203
171204
nb::class_<MyType>(m, "MyType")
172-
.def("__init__", [](MyType *t) { new (t) MyType(...); });
205+
.def("__init__", [](MyType *t, int value) {
206+
auto* self = new (t) MyType(...);
207+
self->value = value;
208+
});
173209
174210
The provided lambda function will be called with a pointer to uninitialized
175211
memory that has already been allocated (this memory region is co-located
@@ -178,15 +214,6 @@ then either run an in-place constructor and return normally (in which case
178214
the instance is assumed to be correctly constructed) or fail by raising an
179215
exception.
180216

181-
To turn an existing factory function into a constructor, you will need to
182-
combine the above pattern with an invocation of the move/copy-constructor,
183-
e.g.:
184-
185-
.. code-block:: cpp
186-
187-
nb::class_<MyType>(m, "MyType")
188-
.def("__init__", [](MyType *t) { new (t) MyType(MyType::create()); });
189-
190217
Implicit conversions
191218
--------------------
192219

include/nanobind/nb_class.h

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,8 @@ struct is_copy_constructible : std::is_copy_constructible<T> { };
278278
template <typename T>
279279
constexpr bool is_copy_constructible_v = is_copy_constructible<T>::value;
280280

281+
struct init_using_factory_tag {};
282+
281283
NAMESPACE_END(detail)
282284

283285
// Low level access to nanobind type objects
@@ -329,7 +331,6 @@ inline void *type_get_slot(handle h, int slot_id) {
329331
#endif
330332
}
331333

332-
333334
template <typename... Args> struct init {
334335
template <typename T, typename... Ts> friend class class_;
335336
NB_INLINE init() {}
@@ -355,6 +356,103 @@ template <typename... Args> struct init {
355356
}
356357
};
357358

359+
template <typename... Args>
360+
struct init<detail::init_using_factory_tag, Args...> {
361+
static_assert(sizeof...(Args) == 2 || sizeof...(Args) == 4,
362+
"Unexpected instantiation convention for factory init");
363+
static_assert(sizeof...(Args) != 2,
364+
"Couldn't deduce function signature for factory function");
365+
static_assert(sizeof...(Args) != 4,
366+
"Base factory and alias factory accept different arguments, "
367+
"or we couldn't otherwise deduce their signatures");
368+
};
369+
370+
template <typename Func, typename Return, typename... Args>
371+
struct init<detail::init_using_factory_tag, Func, Return(Args...)> {
372+
std::remove_reference_t<Func> func;
373+
374+
init(Func &&f) : func((detail::forward_t<Func>) f) {}
375+
376+
template <typename Class, typename... Extra>
377+
NB_INLINE void execute(Class &cl, const Extra&... extra) {
378+
using Type = typename Class::Type;
379+
using Alias = typename Class::Alias;
380+
constexpr bool has_alias = !std::is_same_v<Type, Alias>;
381+
if constexpr (!has_alias) {
382+
static_assert(std::is_constructible_v<Type, Return>,
383+
"nb::init() factory function must return an instance "
384+
"of the type by value, or something that can "
385+
"direct-initialize it");
386+
} else {
387+
static_assert(std::is_constructible_v<Alias, Return>,
388+
"nb::init() factory function must return an instance "
389+
"of the alias type by value, or something that can "
390+
"direct-initialize it");
391+
}
392+
cl.def(
393+
"__init__",
394+
[func_ = (detail::forward_t<Func>) func](pointer_and_handle<Type> v, Args... args) {
395+
if constexpr (has_alias && std::is_constructible_v<Type, Return>) {
396+
if (!detail::nb_inst_python_derived(v.h.ptr())) {
397+
new (v.p) Type{ func_((detail::forward_t<Args>) args...) };
398+
return;
399+
}
400+
}
401+
new ((void *) v.p) Alias{ func_((detail::forward_t<Args>) args...) };
402+
},
403+
extra...);
404+
}
405+
};
406+
407+
template <typename CFunc, typename CReturn, typename AFunc, typename AReturn,
408+
typename... Args>
409+
struct init<detail::init_using_factory_tag, CFunc, CReturn(Args...),
410+
AFunc, AReturn(Args...)> {
411+
std::remove_reference_t<CFunc> cfunc;
412+
std::remove_reference_t<AFunc> afunc;
413+
414+
init(CFunc &&cf, AFunc &&af)
415+
: cfunc((detail::forward_t<CFunc>) cf),
416+
afunc((detail::forward_t<AFunc>) af) {}
417+
418+
template <typename Class, typename... Extra>
419+
NB_INLINE void execute(Class &cl, const Extra&... extra) {
420+
using Type = typename Class::Type;
421+
using Alias = typename Class::Alias;
422+
static_assert(!std::is_same_v<Type, Alias>,
423+
"The form of nb::init() that takes two factory functions "
424+
"doesn't make sense to use on classes that don't have an "
425+
"alias type");
426+
static_assert(std::is_constructible_v<Type, CReturn>,
427+
"nb::init() first factory function must return an "
428+
"instance of the type by value, or something that can "
429+
"direct-initialize it");
430+
static_assert(std::is_constructible_v<Alias, AReturn>,
431+
"nb::init() second factory function must return an "
432+
"instance of the alias type by value, or something that "
433+
"can direct-initialize it");
434+
cl.def(
435+
"__init__",
436+
[cfunc_ = (detail::forward_t<CFunc>) cfunc,
437+
afunc_ = (detail::forward_t<AFunc>) afunc](pointer_and_handle<Type> v, Args... args) {
438+
if (!detail::nb_inst_python_derived(v.h.ptr()))
439+
new (v.p) Type{ cfunc_((detail::forward_t<Args>) args...) };
440+
else
441+
new ((void *) v.p) Alias{ afunc_((detail::forward_t<Args>) args...) };
442+
},
443+
extra...);
444+
}
445+
};
446+
447+
template <typename Func>
448+
init(Func&& f) -> init<detail::init_using_factory_tag,
449+
Func, detail::function_signature_t<Func>>;
450+
451+
template <typename CFunc, typename AFunc>
452+
init(CFunc&& cf, AFunc&& af) -> init<detail::init_using_factory_tag,
453+
CFunc, detail::function_signature_t<CFunc>,
454+
AFunc, detail::function_signature_t<AFunc>>;
455+
358456
template <typename Arg> struct init_implicit {
359457
template <typename T, typename... Ts> friend class class_;
360458
NB_INLINE init_implicit() { }

0 commit comments

Comments
 (0)