Skip to content

Commit 459d0f6

Browse files
committed
Support nb::init(<lambda>) as syntactic sugar for custom constructors
1 parent d793091 commit 459d0f6

8 files changed

+295
-52
lines changed

docs/api_core.rst

+64-25
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

@@ -2567,62 +2572,96 @@ Class binding
25672572
constructor. It is only meant to be used in binding declarations done via
25682573
:cpp:func:`class_::def()`.
25692574

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

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

25982635
.. cpp:struct:: template <typename Arg> init_implicit
25992636

26002637
See :cpp:class:`init` for detail on binding constructors. The main
2601-
difference between :cpp:class:`init` and `init_implicit` is that the latter
2602-
only supports constructors taking a single argument `Arg`, and that it marks
2603-
the constructor as usable for implicit conversions from `Arg`.
2638+
difference between :cpp:class:`init` and `init_implicit` is that the latter
2639+
only supports constructors that exist in the C++ code and take a single
2640+
argument `Arg`, and that it marks the constructor as usable for implicit
2641+
conversions from `Arg`.
26042642

26052643
Sometimes, it is necessary to bind implicit conversion-capable constructors
26062644
that don't exist in the underlying C++ type (meaning that they are specific
2607-
to the Python bindings). This can be done manually noting that
2645+
to the Python bindings). This can be done manually, noting that
26082646

26092647
.. code-block:: cpp
26102648
2611-
nb::class_<MyType>(m, "MyType")
2612-
.def(nb::init_implicit<const char*>());
2649+
nb::class_<MyType>(m, "MyType")
2650+
.def(nb::init_implicit<const char*>());
26132651
26142652
can be replaced by the lower-level code
26152653

26162654
.. code-block:: cpp
26172655
26182656
nb::class_<MyType>(m, "MyType")
2619-
.def("__init__",
2620-
[](MyType* t, const char* arg0) {
2621-
new (t) MyType(arg0);
2622-
});
2657+
.def(nb::init<const char*>());
26232658
26242659
nb::implicitly_convertible<const char*, MyType>();
26252660
2661+
and that this transformation works equally well if you use one of the forms
2662+
of :cpp:class:`nb::init() <init>` that cannot be expressed by
2663+
:cpp:class:`init_implicit`.
2664+
26262665
.. cpp:struct:: template <typename Func> new_
26272666

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

docs/changelog.rst

+11
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,17 @@ Version TBD (not yet released)
7373
binding abstractions that "feel like" the built-in ones.
7474
(PR `#884 <https://github.com/wjakob/nanobind/pull/884>`__)
7575

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

docs/classes.rst

+2-1
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

+45-18
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

+102
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
@@ -365,6 +367,106 @@ template <typename... Args> struct init : def_visitor<init<Args...>> {
365367
}
366368
};
367369

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

0 commit comments

Comments
 (0)