Class Virtual Functions

In this section, we shall learn how to make functions behave polymorphically through virtual functions. Continuing our example, let us add a virtual function to our Base class:

    struct Base
    {
        virtual int f() = 0;
    };

Since f is a pure virtual function, Base is now an abstract class. Given an instance of our class, the free function call_f calls some implementation of this virtual function in a concrete derived class:

    int call_f(Base& b) { return b.f(); }

To allow this function to be implemented in a Python derived class, we need to create a class wrapper:

    struct BaseWrap : Base
    {
        BaseWrap(PyObject* self_)
            : self(self_) {}
        int f() { return call_method<int>(self, "f"); }
        PyObject* self;
    };
member function and methods

Python, like many object oriented languages uses the term methods. Methods correspond roughly to C++'s member functions

Our class wrapper BaseWrap is derived from Base. Its overridden virtual member function f in effect calls the corresponding method of the Python object self, which is a pointer back to the Python Base object holding our BaseWrap instance.

Why do we need BaseWrap?

You may ask, "Why do we need the BaseWrap derived class? This could have been designed so that everything gets done right inside of Base."

One of the goals of Boost.Python is to be minimally intrusive on an existing C++ design. In principle, it should be possible to expose the interface for a 3rd party library without changing it. To unintrusively hook into the virtual functions so that a Python override may be called, we must use a derived class.

Note however that you don't need to do this to get methods overridden in Python to behave virtually when called from Python. The only time you need to do the BaseWrap dance is when you have a virtual function that's going to be overridden in Python and called polymorphically from C++.

Wrapping Base and the free function call_f:

    class_<Base, BaseWrap, boost::noncopyable>("Base", no_init)
        ;
    def("call_f", call_f);

Notice that we parameterized the class_ template with BaseWrap as the second parameter. What is noncopyable? Without it, the library will try to create code for converting Base return values of wrapped functions to Python. To do that, it needs Base's copy constructor... which isn't available, since Base is an abstract class.

In Python, let us try to instantiate our Base class:

    >>> base = Base()
    AttributeError: ...

Why is it an error? Base is an abstract class. As such it is advisable to define the Python wrapper with no_init as we have done above. Doing so will disallow abstract base classes such as Base to be instantiated.

Deriving a Python class

Now, at last, we can even derive from our base class Base in Python:

    >>> class Derived(Base):
    ...     def f(self):
    ...         return 42
    ...

Cool eh? A Python class deriving from a C++ class!

Let's now make an instance of our Python class Derived:

    >>> derived = Derived()

Calling derived.f():

    >>> derived.f()
    42

Will yield the expected result. Finally, calling calling the free function call_f with derived as argument:

    >>> call_f(derived)
    42

Will also yield the expected result.

Here's what's happening:

  1. call_f(derived) is called in Python
  2. This corresponds to def("call_f", call_f);. Boost.Python dispatches this call.
  3. int call_f(Base& b) { return b.f(); } accepts the call.
  4. The overridden virtual function f of BaseWrap is called.
  5. call_method<int>(self, "f"); dispatches the call back to Python.
  6. def f(self): return 42 is finally called.

Rewind back to our Base class, if its member function f was not declared as pure virtual:

    struct Base
    {
        virtual int f() { return 0; }
    };

And instead is implemented to return 0, as shown above.

    struct BaseWrap : Base
    {
        BaseWrap(PyObject* self_)
            : self(self_) {}
        int f() { return call_method<int>(self, "f"); }
        static int default_f(Base* b) { return b->Base::f(); } // <<=== added
        PyObject* self;
    };

then, our Boost.Python wrapper:

    class_<Base, BaseWrap>("Base")
        .def("f", &BaseWrap::default_f)
        ;

Note that we are allowing Base objects to be instantiated this time, unlike before where we specifically defined the class_<Base> with no_init.

In Python, the results would be as expected:

    >>> base = Base()
    >>> class Derived(Base):
    ...     def f(self):
    ...         return 42
    ...
    >>> derived = Derived()

Calling base.f():

    >>> base.f()
    0

Calling derived.f():

    >>> derived.f()
    42

Calling call_f, passing in a base object:

    >>> call_f(base)
    0

Calling call_f, passing in a derived object:

    >>> call_f(derived)
    42