Merge pull request #403 from jagerman/alias-initialization

Implement py::init_alias<>() constructors
This commit is contained in:
Wenzel Jakob 2016-09-10 16:12:19 +09:00 committed by GitHub
commit 1f2e417d8c
7 changed files with 190 additions and 56 deletions

View File

@ -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 See the file :file:`tests/test_virtual_functions.cpp` for complete examples
using both the duplication and templated trampoline approaches. 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<Args, ...>()`` instead of the usual
``py::init<Args, ...>()``. 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: .. _macro_notes:
General notes regarding convenience macros General notes regarding convenience macros

View File

@ -1112,17 +1112,17 @@ private:
NAMESPACE_BEGIN(detail) NAMESPACE_BEGIN(detail)
template <typename... Args> struct init { template <typename... Args> struct init {
template <typename Class, typename... Extra, typename std::enable_if<!Class::has_alias, int>::type = 0> template <typename Class, typename... Extra, enable_if_t<!Class::has_alias, int> = 0>
void execute(Class &cl, const Extra&... extra) const { static void execute(Class &cl, const Extra&... extra) {
using Base = typename Class::type; using Base = typename Class::type;
/// Function which calls a specific C++ in-place constructor /// Function which calls a specific C++ in-place constructor
cl.def("__init__", [](Base *self_, Args... args) { new (self_) Base(args...); }, extra...); cl.def("__init__", [](Base *self_, Args... args) { new (self_) Base(args...); }, extra...);
} }
template <typename Class, typename... Extra, template <typename Class, typename... Extra,
typename std::enable_if<Class::has_alias && enable_if_t<Class::has_alias &&
std::is_constructible<typename Class::type, Args...>::value, int>::type = 0> std::is_constructible<typename Class::type, Args...>::value, int> = 0>
void execute(Class &cl, const Extra&... extra) const { static void execute(Class &cl, const Extra&... extra) {
using Base = typename Class::type; using Base = typename Class::type;
using Alias = typename Class::type_alias; using Alias = typename Class::type_alias;
handle cl_type = cl; handle cl_type = cl;
@ -1135,14 +1135,22 @@ template <typename... Args> struct init {
} }
template <typename Class, typename... Extra, template <typename Class, typename... Extra,
typename std::enable_if<Class::has_alias && enable_if_t<Class::has_alias &&
!std::is_constructible<typename Class::type, Args...>::value, int>::type = 0> !std::is_constructible<typename Class::type, Args...>::value, int> = 0>
void execute(Class &cl, const Extra&... extra) const { static void execute(Class &cl, const Extra&... extra) {
init_alias<Args...>::execute(cl, extra...);
}
};
template <typename... Args> struct init_alias {
template <typename Class, typename... Extra,
enable_if_t<Class::has_alias && std::is_constructible<typename Class::type_alias, Args...>::value, int> = 0>
static void execute(Class &cl, const Extra&... extra) {
using Alias = typename Class::type_alias; using Alias = typename Class::type_alias;
cl.def("__init__", [](Alias *self_, Args... args) { new (self_) Alias(args...); }, extra...); cl.def("__init__", [](Alias *self_, Args... args) { new (self_) Alias(args...); }, extra...);
} }
}; };
inline void keep_alive_impl(handle nurse, handle patient) { inline void keep_alive_impl(handle nurse, handle patient) {
/* Clever approach based on weak references taken from Boost.Python */ /* Clever approach based on weak references taken from Boost.Python */
if (!nurse || !patient) if (!nurse || !patient)
@ -1177,6 +1185,7 @@ struct iterator_state {
NAMESPACE_END(detail) NAMESPACE_END(detail)
template <typename... Args> detail::init<Args...> init() { return detail::init<Args...>(); } template <typename... Args> detail::init<Args...> init() { return detail::init<Args...>(); }
template <typename... Args> detail::init_alias<Args...> init_alias() { return detail::init_alias<Args...>(); }
template <typename Iterator, template <typename Iterator,
typename Sentinel, typename Sentinel,

View File

@ -4,6 +4,7 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
endif() endif()
set(PYBIND11_TEST_FILES set(PYBIND11_TEST_FILES
test_alias_initialization.cpp
test_buffers.cpp test_buffers.cpp
test_callbacks.cpp test_callbacks.cpp
test_class_args.cpp test_class_args.cpp

View File

@ -0,0 +1,62 @@
/*
tests/test_alias_initialization.cpp -- test cases and example of different trampoline
initialization modes
Copyright (c) 2016 Wenzel Jakob <wenzel.jakob@epfl.ch>, Jason Rhinelander <jason@imaginary.ca>
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_<A, PyA>(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_<A2, PyA2>(m, "A2")
.def(py::init_alias<>())
.def("f", &A2::f);
m.def("call_f", [](A2 *a2) { a2->f(); });
});

View File

@ -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()
"""

View File

@ -134,30 +134,6 @@ void init_issues(py::module &m) {
m2.def("expect_float", [](float f) { return f; }); m2.def("expect_float", [](float f) { return f; });
m2.def("expect_int", [](int i) { return i; }); 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_<A, std::unique_ptr<A>, PyA>(m2, "A")
.def(py::init<>())
.def("f", &A::f);
m2.def("call_f", call_f);
try { try {
py::class_<Placeholder>(m2, "Placeholder"); py::class_<Placeholder>(m2, "Placeholder");
throw std::logic_error("Expected an exception!"); throw std::logic_error("Expected an exception!");

View File

@ -79,30 +79,6 @@ def test_no_id(capture, msg):
""" """
assert expect_float(12) == 12 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): def test_str_issue(msg):