diff --git a/docs/advanced.rst b/docs/advanced.rst index ff85a4fc2..382fdf3e9 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -480,6 +480,36 @@ can now create a python class that inherits from ``Dog``: See the file :file:`tests/test_virtual_functions.cpp` for complete examples using both the duplication and templated trampoline approaches. +Extended trampoline class functionality +======================================= + +The trampoline classes described in the previous sections are, by default, only +initialized when needed. More specifically, they are initialized when a python +class actually inherits from a registered type (instead of merely creating an +instance of the registered type), or when a registered constructor is only +valid for the trampoline class but not the registered class. This is primarily +for performance reasons: when the trampoline class is not needed for anything +except virtual method dispatching, not initializing the trampoline class +improves performance by avoiding needing to do a run-time check to see if the +inheriting python instance has an overloaded method. + +Sometimes, however, it is useful to always initialize a trampoline class as an +intermediate class that does more than just handle virtual method dispatching. +For example, such a class might perform extra class initialization, extra +destruction operations, and might define new members and methods to enable a +more python-like interface to a class. + +In order to tell pybind11 that it should *always* initialize the trampoline +class when creating new instances of a type, the class constructors should be +declared using ``py::init_alias()`` instead of the usual +``py::init()``. This forces construction via the trampoline class, +ensuring member initialization and (eventual) destruction. + +.. seealso:: + + See the file :file:`tests/test_alias_initialization.cpp` for complete examples + showing both normal and forced trampoline instantiation. + .. _macro_notes: General notes regarding convenience macros diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index ea7acb4d5..d3be7e214 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1112,17 +1112,17 @@ private: NAMESPACE_BEGIN(detail) template struct init { - template ::type = 0> - void execute(Class &cl, const Extra&... extra) const { + template = 0> + static void execute(Class &cl, const Extra&... extra) { using Base = typename Class::type; /// Function which calls a specific C++ in-place constructor cl.def("__init__", [](Base *self_, Args... args) { new (self_) Base(args...); }, extra...); } template ::value, int>::type = 0> - void execute(Class &cl, const Extra&... extra) const { + enable_if_t::value, int> = 0> + static void execute(Class &cl, const Extra&... extra) { using Base = typename Class::type; using Alias = typename Class::type_alias; handle cl_type = cl; @@ -1135,14 +1135,22 @@ template struct init { } template ::value, int>::type = 0> - void execute(Class &cl, const Extra&... extra) const { + enable_if_t::value, int> = 0> + static void execute(Class &cl, const Extra&... extra) { + init_alias::execute(cl, extra...); + } +}; +template struct init_alias { + template ::value, int> = 0> + static void execute(Class &cl, const Extra&... extra) { using Alias = typename Class::type_alias; cl.def("__init__", [](Alias *self_, Args... args) { new (self_) Alias(args...); }, extra...); } }; + inline void keep_alive_impl(handle nurse, handle patient) { /* Clever approach based on weak references taken from Boost.Python */ if (!nurse || !patient) @@ -1177,6 +1185,7 @@ struct iterator_state { NAMESPACE_END(detail) template detail::init init() { return detail::init(); } +template detail::init_alias init_alias() { return detail::init_alias(); } template , Jason Rhinelander + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ + +#include "pybind11_tests.h" + +test_initializer alias_initialization([](py::module &m) { + // don't invoke Python dispatch classes by default when instantiating C++ classes that were not + // extended on the Python side + struct A { + virtual ~A() {} + virtual void f() { py::print("A.f()"); } + }; + + struct PyA : A { + PyA() { py::print("PyA.PyA()"); } + ~PyA() { py::print("PyA.~PyA()"); } + + void f() override { + py::print("PyA.f()"); + PYBIND11_OVERLOAD(void, A, f); + } + }; + + auto call_f = [](A *a) { a->f(); }; + + py::class_(m, "A") + .def(py::init<>()) + .def("f", &A::f); + + m.def("call_f", call_f); + + + // ... unless we explicitly request it, as in this example: + struct A2 { + virtual ~A2() {} + virtual void f() { py::print("A2.f()"); } + }; + + struct PyA2 : A2 { + PyA2() { py::print("PyA2.PyA2()"); } + ~PyA2() { py::print("PyA2.~PyA2()"); } + void f() override { + py::print("PyA2.f()"); + PYBIND11_OVERLOAD(void, A2, f); + } + }; + + py::class_(m, "A2") + .def(py::init_alias<>()) + .def("f", &A2::f); + + m.def("call_f", [](A2 *a2) { a2->f(); }); + +}); + diff --git a/tests/test_alias_initialization.py b/tests/test_alias_initialization.py new file mode 100644 index 000000000..b6d9e84ca --- /dev/null +++ b/tests/test_alias_initialization.py @@ -0,0 +1,80 @@ +import pytest +import gc + +def test_alias_delay_initialization(capture, msg): + + # A only initializes its trampoline class when we inherit from it; if we + # just create and use an A instance directly, the trampoline initialization + # is bypassed and we only initialize an A() instead (for performance + # reasons) + from pybind11_tests import A, call_f + + class B(A): + def __init__(self): + super(B, self).__init__() + + def f(self): + print("In python f()") + + # C++ version + with capture: + a = A() + call_f(a) + del a + gc.collect() + assert capture == "A.f()" + + # Python version + with capture: + b = B() + call_f(b) + del b + gc.collect() + assert capture == """ + PyA.PyA() + PyA.f() + In python f() + PyA.~PyA() + """ + +def test_alias_delay_initialization(capture, msg): + from pybind11_tests import A2, call_f + + # A2, unlike the above, is configured to always initialize the alias; while + # the extra initialization and extra class layer has small virtual dispatch + # performance penalty, it also allows us to do more things with the + # trampoline class such as defining local variables and performing + # construction/destruction. + + class B2(A2): + def __init__(self): + super(B2, self).__init__() + + def f(self): + print("In python B2.f()") + + # No python subclass version + with capture: + a2 = A2() + call_f(a2) + del a2 + gc.collect() + assert capture == """ + PyA2.PyA2() + PyA2.f() + A2.f() + PyA2.~PyA2() + """ + + # Python subclass version + with capture: + b2 = B2() + call_f(b2) + del b2 + gc.collect() + assert capture == """ + PyA2.PyA2() + PyA2.f() + In python B2.f() + PyA2.~PyA2() + """ diff --git a/tests/test_issues.cpp b/tests/test_issues.cpp index 843978eff..3fc8aeb66 100644 --- a/tests/test_issues.cpp +++ b/tests/test_issues.cpp @@ -134,30 +134,6 @@ void init_issues(py::module &m) { m2.def("expect_float", [](float f) { return f; }); m2.def("expect_int", [](int i) { return i; }); - // (no id): don't invoke Python dispatch code when instantiating C++ - // classes that were not extended on the Python side - struct A { - virtual ~A() {} - virtual void f() { py::print("A.f()"); } - }; - - struct PyA : A { - PyA() { py::print("PyA.PyA()"); } - - void f() override { - py::print("PyA.f()"); - PYBIND11_OVERLOAD(void, A, f); - } - }; - - auto call_f = [](A *a) { a->f(); }; - - pybind11::class_, PyA>(m2, "A") - .def(py::init<>()) - .def("f", &A::f); - - m2.def("call_f", call_f); - try { py::class_(m2, "Placeholder"); throw std::logic_error("Expected an exception!"); diff --git a/tests/test_issues.py b/tests/test_issues.py index a28e50902..b4b8f95ff 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -79,30 +79,6 @@ def test_no_id(capture, msg): """ assert expect_float(12) == 12 - from pybind11_tests.issues import A, call_f - - class B(A): - def __init__(self): - super(B, self).__init__() - - def f(self): - print("In python f()") - - # C++ version - with capture: - a = A() - call_f(a) - assert capture == "A.f()" - - # Python version - with capture: - b = B() - call_f(b) - assert capture == """ - PyA.PyA() - PyA.f() - In python f() - """ def test_str_issue(msg):