Skip to content

Commit f16a7af

Browse files
allow cast of C++ class in its ctor
allocate memory before calling the ctor register the instance before calling the ctor use a placement new call to set the value_ptr use `prealloacte` ennotation to select placement new refactor construct_or_initialize to construct inplace add a type trait to check if a template param pack contains a type add tests add documentation
1 parent 88a1bb9 commit f16a7af

File tree

6 files changed

+238
-21
lines changed

6 files changed

+238
-21
lines changed

docs/advanced/classes.rst

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,3 +1333,100 @@ You can do that using ``py::custom_type_setup``:
13331333
cls.def_readwrite("value", &OwnsPythonObjects::value);
13341334
13351335
.. versionadded:: 2.8
1336+
1337+
Accessing Python object from C++
1338+
================================
1339+
1340+
In advanced cases, it's really handy to access the Python sibling of
1341+
a C++ object to get/set attributes, do some heavy computations, just hide
1342+
the implementation details, or even dynamically create a new attribute!
1343+
1344+
One just need to rely on the `casting capabilities`_ of ``pybind11``:
1345+
1346+
.. code-block:: cpp
1347+
1348+
// main.cpp
1349+
#include "pybind11/pybind11.h"
1350+
1351+
namespace py = pybind11;
1352+
1353+
class Base {
1354+
public:
1355+
Base() = default;
1356+
std::string get_foo() { return py::cast(this).attr("foo").cast<std::string>(); };
1357+
void set_bar() { py::cast(this).attr("bar") = 10.; };
1358+
};
1359+
1360+
PYBIND11_MODULE(test, m) {
1361+
py::class_<Base>(m, "Base")
1362+
.def(py::init<>())
1363+
.def("get_foo", &Base::get_foo)
1364+
.def("set_bar", &Base::set_bar);
1365+
}
1366+
1367+
1368+
.. code-block:: python
1369+
1370+
# test.py
1371+
from test import Base
1372+
1373+
1374+
class Derived(Base):
1375+
def __init__(self):
1376+
Base.__init__(self)
1377+
self.foo = "hello"
1378+
1379+
1380+
b = Derived()
1381+
assert b.get_foo() == "hello"
1382+
assert not hasattr(b, "bar")
1383+
1384+
b.set_bar()
1385+
assert b.bar == 10.0
1386+
1387+
1388+
However, there is a special case where such a behavior needs a hint to work as expected:
1389+
when the C++ constructor is called, the C++ object is not yet allocated and registered,
1390+
making impossible the casting operation.
1391+
1392+
It's thus impossible to access the Python object from the C++ constructor one *as is*.
1393+
1394+
Adding the ``py::preallocate()`` extra option to a constructor binding definition informs
1395+
``pybind11`` to allocate the memory and register the object location just before calling the C++
1396+
constructor, enabling the use of ``py::cast(this)``:
1397+
1398+
1399+
.. code-block:: cpp
1400+
1401+
// main.cpp
1402+
#include "pybind11/pybind11.h"
1403+
1404+
namespace py = pybind11;
1405+
1406+
class Base {
1407+
public:
1408+
Base() { py::cast(this).attr("bar") = 10.; };
1409+
};
1410+
1411+
PYBIND11_MODULE(test, m) {
1412+
py::class_<Base>(m, "Base")
1413+
.def(py::init<>(), py::preallocate());
1414+
}
1415+
1416+
1417+
.. code-block:: python
1418+
1419+
# test.py
1420+
from test import Base
1421+
1422+
1423+
class Derived(Base):
1424+
...
1425+
1426+
1427+
b = Derived()
1428+
assert hasattr(b, "bar")
1429+
assert b.bar == 10.0
1430+
1431+
1432+
.. _casting capabilities: https://pybind11.readthedocs.io/en/stable/advanced/pycpp/object.html?highlight=cast#casting-back-and-forth

include/pybind11/attr.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ struct keep_alive {};
7272
/// Annotation indicating that a class is involved in a multiple inheritance relationship
7373
struct multiple_inheritance {};
7474

75+
/// Annotation indicating that a class should be preallocated and registered before construction
76+
struct preallocate {};
77+
7578
/// Annotation which enables dynamic attributes, i.e. adds `__dict__` to a class
7679
struct dynamic_attr {};
7780

@@ -438,6 +441,11 @@ struct process_attribute<is_operator> : process_attribute_default<is_operator> {
438441
static void init(const is_operator &, function_record *r) { r->is_operator = true; }
439442
};
440443

444+
template <>
445+
struct process_attribute<preallocate> : process_attribute_default<preallocate> {
446+
static void init(const preallocate &, function_record *) {}
447+
};
448+
441449
template <>
442450
struct process_attribute<is_new_style_constructor>
443451
: process_attribute_default<is_new_style_constructor> {

include/pybind11/detail/common.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,18 @@ using is_lambda = satisfies_none_of<remove_reference_t<T>,
885885
std::is_pointer,
886886
std::is_member_pointer>;
887887

888+
/// Check if T is part of a template parameter pack
889+
template <typename T, typename... List>
890+
struct contains : std::true_type {};
891+
892+
template <typename T, typename Head, typename... Rest>
893+
struct contains<T, Head, Rest...>
894+
: std::conditional<std::is_same<T, Head>::value, std::true_type, contains<T, Rest...>>::type {
895+
};
896+
897+
template <typename T>
898+
struct contains<T> : std::false_type {};
899+
888900
// [workaround(intel)] Internal error on fold expression
889901
/// Apply a function over each element of a parameter pack
890902
#if defined(__cpp_fold_expressions) && !defined(__INTEL_COMPILER)

include/pybind11/detail/init.h

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -61,24 +61,53 @@ constexpr bool is_alias(void *) {
6161
return false;
6262
}
6363

64-
// Constructs and returns a new object; if the given arguments don't map to a constructor, we fall
64+
// Constructs a new object inplace; if the given arguments don't map to a constructor, we fall
6565
// back to brace aggregate initiailization so that for aggregate initialization can be used with
6666
// py::init, e.g. `py::init<int, int>` to initialize a `struct T { int a; int b; }`. For
6767
// non-aggregate types, we need to use an ordinary T(...) constructor (invoking as `T{...}` usually
6868
// works, but will not do the expected thing when `T` has an `initializer_list<T>` constructor).
69-
template <typename Class,
70-
typename... Args,
71-
detail::enable_if_t<std::is_constructible<Class, Args...>::value, int> = 0>
72-
inline Class *construct_or_initialize(Args &&...args) {
73-
return new Class(std::forward<Args>(args)...);
69+
template <
70+
typename Class,
71+
bool Preallocate,
72+
typename... Args,
73+
detail::enable_if_t<std::is_constructible<Class, Args...>::value && !Preallocate, int> = 0>
74+
inline void construct_or_initialize(value_and_holder &v_h, Args &&...args) {
75+
v_h.value_ptr() = new Class(std::forward<Args>(args)...);
7476
}
75-
template <typename Class,
76-
typename... Args,
77-
detail::enable_if_t<!std::is_constructible<Class, Args...>::value, int> = 0>
78-
inline Class *construct_or_initialize(Args &&...args) {
79-
return new Class{std::forward<Args>(args)...};
77+
template <
78+
typename Class,
79+
bool Preallocate,
80+
typename... Args,
81+
detail::enable_if_t<!std::is_constructible<Class, Args...>::value && !Preallocate, int> = 0>
82+
inline void construct_or_initialize(value_and_holder &v_h, Args &&...args) {
83+
v_h.value_ptr() = new Class{std::forward<Args>(args)...};
84+
}
85+
// The preallocated variants are performing memory allocation and registration before actually
86+
// calling the constructor to allow casting the C++ pointer to its Python counterpart.
87+
template <
88+
typename Class,
89+
bool Preallocate,
90+
typename... Args,
91+
detail::enable_if_t<std::is_constructible<Class, Args...>::value && Preallocate, int> = 0>
92+
inline void construct_or_initialize(value_and_holder &v_h, Args &&...args) {
93+
v_h.value_ptr() = malloc(sizeof(Class));
94+
register_instance(v_h.inst, v_h.value_ptr(), v_h.type);
95+
v_h.set_instance_registered();
96+
97+
new (v_h.value_ptr<Class>()) Class(std::forward<Args>(args)...);
98+
}
99+
template <
100+
typename Class,
101+
bool Preallocate,
102+
typename... Args,
103+
detail::enable_if_t<!std::is_constructible<Class, Args...>::value && Preallocate, int> = 0>
104+
inline void construct_or_initialize(value_and_holder &v_h, Args &&...args) {
105+
v_h.value_ptr() = malloc(sizeof(Class));
106+
register_instance(v_h.inst, v_h.value_ptr(), v_h.type);
107+
v_h.set_instance_registered();
108+
109+
new (v_h.value_ptr<Class>()) Class{std::forward<Args>(args)...};
80110
}
81-
82111
// Attempts to constructs an alias using a `Alias(Cpp &&)` constructor. This allows types with
83112
// an alias to provide only a single Cpp factory function as long as the Alias can be
84113
// constructed from an rvalue reference of the base Cpp type. This means that Alias classes
@@ -200,7 +229,8 @@ struct constructor {
200229
cl.def(
201230
"__init__",
202231
[](value_and_holder &v_h, Args... args) {
203-
v_h.value_ptr() = construct_or_initialize<Cpp<Class>>(std::forward<Args>(args)...);
232+
construct_or_initialize<Cpp<Class>, contains<preallocate, Extra...>::value>(
233+
v_h, std::forward<Args>(args)...);
204234
},
205235
is_new_style_constructor(),
206236
extra...);
@@ -215,11 +245,11 @@ struct constructor {
215245
"__init__",
216246
[](value_and_holder &v_h, Args... args) {
217247
if (Py_TYPE(v_h.inst) == v_h.type->type) {
218-
v_h.value_ptr()
219-
= construct_or_initialize<Cpp<Class>>(std::forward<Args>(args)...);
248+
construct_or_initialize<Cpp<Class>, contains<preallocate, Extra...>::value>(
249+
v_h, std::forward<Args>(args)...);
220250
} else {
221-
v_h.value_ptr()
222-
= construct_or_initialize<Alias<Class>>(std::forward<Args>(args)...);
251+
construct_or_initialize<Alias<Class>, contains<preallocate, Extra...>::value>(
252+
v_h, std::forward<Args>(args)...);
223253
}
224254
},
225255
is_new_style_constructor(),
@@ -234,8 +264,8 @@ struct constructor {
234264
cl.def(
235265
"__init__",
236266
[](value_and_holder &v_h, Args... args) {
237-
v_h.value_ptr()
238-
= construct_or_initialize<Alias<Class>>(std::forward<Args>(args)...);
267+
construct_or_initialize<Alias<Class>, contains<preallocate, Extra...>::value>(
268+
v_h, std::forward<Args>(args)...);
239269
},
240270
is_new_style_constructor(),
241271
extra...);
@@ -253,8 +283,8 @@ struct alias_constructor {
253283
cl.def(
254284
"__init__",
255285
[](value_and_holder &v_h, Args... args) {
256-
v_h.value_ptr()
257-
= construct_or_initialize<Alias<Class>>(std::forward<Args>(args)...);
286+
construct_or_initialize<Alias<Class>, contains<preallocate, Extra...>::value>(
287+
v_h, std::forward<Args>(args)...);
258288
},
259289
is_new_style_constructor(),
260290
extra...);

tests/test_methods_and_attributes.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,20 @@ struct RValueRefParam {
177177
std::size_t func4(std::string &&s) const & { return s.size(); }
178178
};
179179

180+
// Test Python init from C++ constructor
181+
struct InitPyFromCpp1 {
182+
InitPyFromCpp1() { py::cast(this).attr("bar") = 10.; };
183+
};
184+
struct InitPyFromCpp2 {
185+
InitPyFromCpp2() { py::cast(this).attr("bar") = 10.; };
186+
};
187+
struct InitPyFromCppDynamic1 {
188+
InitPyFromCppDynamic1() { py::cast(this).attr("bar") = 10.; };
189+
};
190+
struct InitPyFromCppDynamic2 {
191+
InitPyFromCppDynamic2() { py::cast(this).attr("bar") = 10.; };
192+
};
193+
180194
TEST_SUBMODULE(methods_and_attributes, m) {
181195
// test_methods_and_attributes
182196
py::class_<ExampleMandA> emna(m, "ExampleMandA");
@@ -456,4 +470,12 @@ TEST_SUBMODULE(methods_and_attributes, m) {
456470
.def("func2", &RValueRefParam::func2)
457471
.def("func3", &RValueRefParam::func3)
458472
.def("func4", &RValueRefParam::func4);
473+
474+
// Test Python init from C++ constructor
475+
py::class_<InitPyFromCpp1>(m, "InitPyFromCpp1").def(py::init<>());
476+
py::class_<InitPyFromCpp2>(m, "InitPyFromCpp2").def(py::init<>(), py::preallocate());
477+
py::class_<InitPyFromCppDynamic1>(m, "InitPyFromCppDynamic1", py::dynamic_attr())
478+
.def(py::init<>());
479+
py::class_<InitPyFromCppDynamic2>(m, "InitPyFromCppDynamic2", py::dynamic_attr())
480+
.def(py::init<>(), py::preallocate());
459481
}

tests/test_methods_and_attributes.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,3 +525,51 @@ def test_rvalue_ref_param():
525525
assert r.func2("1234") == 4
526526
assert r.func3("12345") == 5
527527
assert r.func4("123456") == 6
528+
529+
530+
@pytest.mark.xfail("env.PYPY")
531+
def test_init_py_from_cpp():
532+
# test dynamically added attr from C++ to Python counterpart
533+
534+
# 1. on class not supporting dynamic attributes
535+
with pytest.raises(AttributeError):
536+
m.InitPyFromCpp1()
537+
538+
with pytest.raises(AttributeError):
539+
m.InitPyFromCpp2()
540+
541+
# 2. on derived class of base not supporting dynamic attributes
542+
class Derived1(m.InitPyFromCpp1):
543+
...
544+
545+
with pytest.raises(AttributeError):
546+
Derived1()
547+
548+
class Derived2(m.InitPyFromCpp2):
549+
...
550+
551+
assert Derived2().bar == 10.0
552+
553+
# 3. on class supporting dynamic attributes
554+
# constructor will set the `bar` attribute to a temporary Python object
555+
a = m.InitPyFromCppDynamic1()
556+
with pytest.raises(AttributeError):
557+
a.bar
558+
559+
# works fine
560+
assert m.InitPyFromCppDynamic2().bar == 10.0
561+
562+
# 4. on derived class of base supporting dynamic attributes
563+
class DynamicDerived1(m.InitPyFromCppDynamic1):
564+
...
565+
566+
# still the same issue
567+
d = DynamicDerived1()
568+
with pytest.raises(AttributeError):
569+
d.bar
570+
571+
# works fine
572+
class DynamicDerived2(m.InitPyFromCppDynamic2):
573+
...
574+
575+
assert DynamicDerived2().bar == 10.0

0 commit comments

Comments
 (0)