It is surprisingly, that despite feeling naturally, there is no straight forward way to make PyB
a subclass of PyA
, - after all B
is a subclass of A
!
However, the desired hierarchy violates the Liskov substitution principle in some subtle ways. This principle says something along the lines:
If B
is a subclass of A
, then the objects of type A
can be
replaced by objects of type B
without breaking the semantics of
program.
It is not directly obvious, because the public interfaces of PyA
and PyB
are ok from Liskov's point of view, but there is one (implicit) property which makes our life harder:
PyA
can wrap any object of type A
PyB
can wrap any object of type B
, also can do less than PyB
!
This observation means there will be no beautiful solution for the problem, and your proposal of using different pointers isn't that bad.
My solution presented bellow has a very similar idea, only that I use a cast rather (which might improve the performance slightly by paying some type-safety), than to cache the pointer.
To make the example stand-alone I use inline-C-verbatim code and to make it more general I use classes without nullable constructors:
%%cython --cplus
cdef extern from *:
"""
#include <iostream>
class A {
protected:
int number;
public:
A(int n):number(n){}
void foo() {std::cout<<"foo "<<number<<std::endl;}
};
class B : public A {
public:
B(int n):A(n){}
void bar() {std::cout<<"bar "<<number<<std::endl;}
};
"""
cdef cppclass A:
A(int n)
void foo()
cdef cppclass B(A): # make clear to Cython, that B inherits from A!
B(int n)
void bar()
...
Differences to your example:
- constructors have a parameter and thus aren't nullable
- I let the Cython know, that
B
is a subclass of A
, i.e. use cdef cppclass B(A)
- thus we can omit castings from B
to A
later on.
Here is the wrapper for class A
:
...
cdef class PyA:
cdef A* thisptr # ptr in order to allow for classes without nullable constructors
cdef void init_ptr(self, A* ptr):
self.thisptr=ptr
def __init__(self, n):
self.init_ptr(new A(n))
def __dealloc__(self):
if NULL != self.thisptr:
del self.thisptr
def foo(self):
self.thisptr.foo()
...
Noteworthy details are:
thisptr
is of type A *
and not A
, because A
has no nullable constructor
- I use raw-pointer (thus
__dealloc__
needed) for holding the reference, maybe one could considered using std::unique_ptr
or std::shared_ptr
, depending on how the class is used.
- When an object of class
A
is created, thisptr
is automatically initialized to nullptr
, so there is no need to explicitly set thisptr
to nullptr
in __cinit__
(which is the reason __cinit__
is omitted).
- Why
__init__
and not __cinit__
is used will become evident in a little while.
And now the wrapper for class B
:
...
cdef class PyB(PyA):
def __init__(self, n):
self.init_ptr(new B(n))
cdef B* as_B(self):
return <B*>(self.thisptr) # I know for sure it is of type B*!
def bar(self):
self.as_B().bar()
Noteworthy details:
as_B
is used to cast thisptr
to B
(which it really is) instead of keeping an cached B *
-pointer.
- There is a subtle difference between
__cinit__
and __init__
: __cinit__
of the parent class will be always called, yet the __init__
of the parent class will only be called, when there is no implementation of the __init__
-method for the class itself. Thus, we use __init__
because we would like to override/omit setting of self.thisptr
of the basis-class.
And now (it prints to std::out and not the ipython-cell!):
>>> PyB(42).foo()
foo 42
>>> PyB(42).bar()
bar 42
One last thought: I did the experience, that using inheritance in order to "save code" often led to problems, because one ended up with "wrong" hierarchies for wrong reasons. There might be another tools to reduce boilerplate code (like pybind11-framework mentioned by @chrisb) that are better for this job.