e70bbe4791
Also port some "glaringly obvious" bugfixes from HEAD. Hope it doesn't cause problems. [SVN r35237]
223 lines
6.2 KiB
Plaintext
223 lines
6.2 KiB
Plaintext
.. Copyright David Abrahams 2006. Distributed under the Boost
|
|
.. Software License, Version 1.0. (See accompanying
|
|
.. file LICENSE_1_0.txt or copy at
|
|
.. http://www.boost.org/LICENSE_1_0.txt)
|
|
|
|
How Runtime Polymorphism is expressed in Boost.Python:
|
|
-----------------------------------------------------
|
|
|
|
struct A { virtual std::string f(); virtual ~A(); };
|
|
|
|
std::string call_f(A& x) { return x.f(); }
|
|
|
|
struct B { virtual std::string f() { return "B"; } };
|
|
|
|
struct Bcb : B
|
|
{
|
|
Bcb(PyObject* self) : m_self(self) {}
|
|
|
|
virtual std::string f() { return call_method<std::string>(m_sef, "f"); }
|
|
static std::string f_default(B& b) { return b.B::f(); }
|
|
|
|
PyObject* m_self;
|
|
};
|
|
|
|
struct C : B
|
|
{
|
|
virtual std::string f() { return "C"; }
|
|
};
|
|
|
|
>>> class D(B):
|
|
... def f():
|
|
... return 'D'
|
|
...
|
|
>>> class E(B): pass
|
|
...
|
|
|
|
|
|
When we write, "invokes B::f non-virtually", we mean:
|
|
|
|
void g(B& x) { x.B::f(); }
|
|
|
|
This will call B::f() regardless of the dynamic type of x. Any other
|
|
way of invoking B::f, including through a function pointer, is a
|
|
"virtual invocation", and will call the most-derived override of f().
|
|
|
|
Case studies
|
|
|
|
C++\Python class
|
|
\___A_____B_____C_____D____E___
|
|
|
|
|
A | 1
|
|
|
|
|
B | 2 3
|
|
|
|
|
Bcb | 4 5 6
|
|
|
|
|
C | 7 8
|
|
|
|
|
|
|
|
|
1. Simple case
|
|
|
|
2. Python A holds a B*. Probably won't happen once we have forced
|
|
downcasting.
|
|
|
|
Requires:
|
|
x.f() -> 'B'
|
|
call_f(x) -> 'B'
|
|
|
|
Implies: A.f invokes A::f() (virtually or otherwise)
|
|
|
|
3. Python B holds a B*.
|
|
|
|
Requires:
|
|
x.f() -> 'B'
|
|
call_f(x) -> 'B'
|
|
|
|
Implies: B.f invokes B::f (virtually or otherwise)
|
|
|
|
|
|
4. B constructed from Python
|
|
|
|
Requires:
|
|
|
|
x.f() -> 'B'
|
|
call_f(x) -> 'B'
|
|
|
|
Implies: B.f invokes B::f non-virtually. Bcb::f invokes B::f
|
|
non-virtually.
|
|
|
|
Question: Does it help if we arrange for Python B construction to
|
|
build a true B object? Then this case doesn't arise.
|
|
|
|
|
|
5. D is a Python class derived from B
|
|
|
|
Requires:
|
|
|
|
x.f() -> 'D'
|
|
call_f(x) -> 'D'
|
|
|
|
Implies: Bcb::f must invoke call_method to look up the Python
|
|
method override, otherwise call_f wouldn't work.
|
|
|
|
6. E is like D, but doesn't override f
|
|
|
|
Requires:
|
|
|
|
x.f() -> 'B'
|
|
call_f(x) -> 'B'
|
|
|
|
Implies: B.f invokes B::f non-virtually. If it were virtual, x.f()
|
|
would cause infinite recursion, because we've already
|
|
determined that Bcb::f must invoke call_method to look up
|
|
the Python method override.
|
|
|
|
7. Python B object holds a C*
|
|
|
|
Requires:
|
|
|
|
x.f() -> 'C'
|
|
call_f(x) -> 'C'
|
|
|
|
Implies: B.f invokes B::f virtually.
|
|
|
|
8. C object constructed from Python
|
|
|
|
Requires:
|
|
|
|
x.f() -> 'C'
|
|
call_f(x) -> 'C'
|
|
|
|
Implies: nothing new.
|
|
|
|
------
|
|
|
|
Total implications:
|
|
|
|
2: A.f invokes A::f() (virtually or otherwise)
|
|
3: B.f invokes B::f (virtually or otherwise)
|
|
4: B.f invokes B::f non-virtually. Bcb::f invokes B::f non-virtually
|
|
6: B.f invokes B::f non-virtually.
|
|
7: B.f invokes B::f virtually.
|
|
|
|
5: Bcb::f invokes call_method to look up the Python method
|
|
|
|
Though (4) is avoidable, clearly 6 and 7 are not, and they
|
|
conflict. The implication is that B.f must choose its behavior
|
|
according to the type of the contained C++ object. If it is Bcb, a
|
|
non-virtual call to B::f must occur. Otherwise, a virtual call to B::f
|
|
must occur. This is essentially the same scheme we had with
|
|
Boost.Python v1.
|
|
|
|
Note: in early versions of Boost.Python v1, we solved this problem by
|
|
introducing a new Python class in the hierarchy, so that D and E
|
|
actually derive from a B', and B'.f invokes B::f non-virtually, while
|
|
B.f invokes B::f virtually. However, people complained about the
|
|
artificial class in the hierarchy, which was revealed when they tried
|
|
to do normal kinds of Python introspection.
|
|
|
|
-------
|
|
|
|
Assumption: we will have a function which builds a virtual function
|
|
dispatch callable Python object.
|
|
|
|
make_virtual_function(pvmf, default_impl, call_policies, dispatch_type)
|
|
|
|
Pseudocode:
|
|
|
|
Get first argument from Python arg tuple
|
|
if it contains dispatch_type
|
|
call default_impl
|
|
else
|
|
call through pvmf
|
|
|
|
|
|
Open questions:
|
|
|
|
1. What about Python multiple inheritance? Do we have the right
|
|
check in the if clause above?
|
|
|
|
A: Not quite. The correct test looks like:
|
|
|
|
Deduce target type of pvmf, i.e. T in R(T::*)(A1...AN).
|
|
Find holder in first argument which holds T
|
|
if it holds dispatch_type...
|
|
|
|
2. Can we make this more efficient?
|
|
|
|
The current "returning" mechanism will look up a holder for T
|
|
again. I don't know if we know how to avoid that.
|
|
|
|
|
|
OK, the solution involves reworking the call mechanism. This is
|
|
neccesary anyway in order to enable wrapping of function objects.
|
|
|
|
It can result in a reduction in the overall amount of source code,
|
|
because returning<> won't need to be specialized for every
|
|
combination of function and member function... though it will still
|
|
need a void specialization. We will still need a way to dispatch to
|
|
member functions through a regular function interface. mem_fn is
|
|
almost the right tool, but it only goes up to 8
|
|
arguments. Forwarding is tricky if you don't want to incur copies.
|
|
I think the trick is to use arg_from_python<T>::result_type for each
|
|
argument to the forwarder.
|
|
|
|
Another option would be to use separate function, function object,
|
|
and member function dispatchers. Once you know you have a member
|
|
function, you don't need cv-qualified overloads to call it.
|
|
|
|
Hmm, while we're at this, maybe we should solve the write-back
|
|
converter problem. Can we do it? Maybe not. Ralf doesn't want to
|
|
write special write-back functions here, does he? He wants the
|
|
converter to do the work automatically. We could add
|
|
cleanup/destructor registration. That would relieve the client from
|
|
having accessible destructors for types which are being converted by
|
|
rvalue. I'm not sure that this will really save any code,
|
|
however. It rather depends on the linker, doesn't it? I wonder if
|
|
this can be done in a backwards-compatible fashion by generating the
|
|
delete function when it's not supplied?
|
|
|
|
|