ascobol
ascobol

Reputation: 7766

Cython and C++ inheritance

I have 2 classes, A and B. B inherits from A.

//C++    
class A
{
    public:
        int getA() {return this->a;};
        A() {this->a = 42;}
    private:
        int a;

};

class B: public A
{
    public:
       B() {this->b = 111;};
       int getB() {return this->b;};
    private:
        int b;

};

Now I would like to interface those two classes using Cython and have the possibility to call the getA() method from a B instance:

a = PyA()
b = PyB()
assert a.getA() == b.getA()

Currently my pyx file looks like this:

cdef extern from "Inherit.h" :
    cdef cppclass A:
       int getA()

    cdef cppclass B(A):
       int getB()


cdef class PyA:
    cdef A* thisptr

    def __cinit__(self):
       print "in A: allocating thisptr"
       self.thisptr = new A()
    def __dealloc__(self):
       if self.thisptr:
           print "in A: deallocating thisptr"
           del self.thisptr

    def getA(self):
       return self.thisptr.getA()

cdef class PyB(PyA):
    def __cinit__(self):
       if self.thisptr:
          print "in B: deallocating old A"
          del self.thisptr
       print "in B: creating new b"
       self.thisptr = new B()

    def __dealloc__(self):
       if self.thisptr:
           print "in B: deallocating thisptr"
           del self.thisptr
           self.thisptr = <A*>0

    def getB(self):
       return (<B*>self.thisptr).getB()

While I hope this code is not doing anything too dangerous, I also hope there is a better way to handle it.

Also using the module generates the following output:

>>> from inherit import *
>>> b = PyB()
in A: allocating thisptr
in B: deallocating old A
in B: creating new b
>>> b.getA()
42
>>> b.getB()
111
>>> del b
in B: deallocating thisptr

And I don't really like allocating an A instance just to free it immediately after.

Any advice about how to do it correctly ?

Upvotes: 13

Views: 5024

Answers (3)

Arpegius
Arpegius

Reputation: 5887

I make some experiments, and have quite ready answer but now i see where is the problem:

If your extension type has a base type, the __cinit__ method of the base type is automatically called before your __cinit__ method is called; you cannot explicitly call the inherited __cinit__ method.

So the real problem is that Cython types still do not have constructors, only pre initializer hook __cinit__ which behave more like default constructors. You cannot call virtual method from constructor and you cannot call it from __cinit__ either (if you make a call, it behave like non virtual).

Somehow inside __cinit__ the type(self) return correct type object, but it is useless. Cython do not have static fields, methods and type object can be only instance of type (no metaclasses). Python @staticmethod is easy overridable, so it is useless.

So there is no other way like to put allocation inside def __init__(self):, and check for initialized thisptr wherever you use it.

You may consider creating a global dummy C++ object, and assign it to thisptr to avoid checking and crashing. There are no post initializer hook, so you will be unable to check if correct initialization already have taken place.

Upvotes: 8

JW Peterson
JW Peterson

Reputation: 325

(I'm new to both Python and Cython, so take this answer for what it's worth.) If you initialize the thisptr in __init__ functions rather than __cinit__ functions, things seem to work in this particular example without the extra allocation/deletion... basically change your __cinit__ functions above to:

def __init__(self):
    print "in A: creating new A"
    self.thisptr = new A()

And

def __init__(self):
    print "in B: creating new B"
    self.thisptr = new B()

respectively. However, I am sure that this is at least theoretically unsafe (and probably practically unsafe as well) but perhaps someone could comment on exactly how unsafe...

For example, from the Cython introduction paper we know that "__init__ is not guaranteed to be run (for instance, one could create a sub-class and forget to call the ancestor constructor)." I have not been able to construct a test case where this occurs, but this is probably due to a general lack of Python knowledge on my part...

Upvotes: 1

Mahmoud Al-Qudsi
Mahmoud Al-Qudsi

Reputation: 29579

I have never laid eyes on Cython before, so please forgive me if this is way off. That said:

In C++, any time you have bar : foo (bar inheriting from foo), if foo has a default constructor, it will be called automatically when bar is created.... unless you call your own custom constructor for the parent.

I don't know Python, but some quick Googling tells me that the same principles apply. i.e. the default constructor for PyA will only be called if PyB doesn't manually call another.

In that case, wouldn't something like this work?

cdef class PyA:
    cdef A* thisptr

    def __cinit__(self, bypasskey="")
       if bypasskey == "somesecret"
           print "in A: bypassing allocation"
       else
           print "in A: allocating thisptr"
           self.thisptr = new A()

...

cdef class PyB(PyA):
    def __cinit__(self):
       super( PyB, self ).__init__("somesecret")

Mind you, I'm sure it's crude. But perhaps the idea here will give you something to work with?


Here's another idea. I'm almost certain it'll work (but that the syntax is off), and it's certainly much cleaner than the above:

cdef class PyA:
    cdef A* thisptr

    def __cinit__(self, t=type(A))
           self.thisptr = new t()

...

cdef class PyB(PyA):
    def __cinit__(self):
       super( PyB, self ).__init__(type(B))

Or maybe it'll look like this?

cdef class PyA:
    cdef A* thisptr

    def __cinit__(self, t=A)
           self.thisptr = new t()

...

cdef class PyB(PyA):
    def __cinit__(self):
       super( PyB, self ).__init__(B)

I'm not posting this for the bounty (and you're not forced to assign it to anyone), I'm just sharing some thoughts with you.

I think you can/should be able to avoid "crashing the interpreter" if you either

a) make the second constructor only visible to b (don't know if this is possible), or

b) check if a is null before using it elsewhere, or

c) check that the calling function was the constructor for b before bypassing allocation.

Also, the Cython C++ documentation makes it rather clear that there may not be idiomatic solutions to all C++ adaptations, with vague, hand-wavy quotes like "It might take some experimenting by others (you?) to find the most elegant ways of handling this issue."

Upvotes: 3

Related Questions