Search code examples
pythonc++boostboost-python

boost-python - expose C++ (non-pure) virtual method with default arguments


In boost-python, given some class X, the recommended way to expose a virtual method is to wrap it as shown below.

I'm trying to marry this with the functionality to specify default parameters on that virtual method. This is also supported in the Boost docs.

However no example is given of exposing a virtual method which also has default parameters.

I've assumed the wrapper class must also define the argument as a default and pass this through to the underlying getItem().

The default argument is a NULL pointer, although I have no reason to suspect (yet) that this is relevant.

struct X_wrap : X, wrapper<X>                                                                                                                                                                                                                  
{                                                                                                                                                                                                                                                                   
    X_wrap(): X() {}                                                                                                                                                                                                                                  

    // getItem() is a non-Pure Virtual Function in Class X
    // It has a single argument a, which has a default value of 1                                                                                                                                                                                                           
    A* getItem(Z* a=NULL)                                                                                                                                                                                                                                              
    {                                                                                                                                                                                                                                                               
        if (override getItem = this->get_override("getItem"))                                                                                                                                                                                                       
            return getItem(a);                                                                                                                                                                                                                                       
        return X::getItem(a);                                                                                                                                                                                                                                 
    }                                                                                                                                                                                                                                                               
    A* default_getItem(Z* a=NULL) { return this->X::getItem(a); }                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
};

This is then defined as:

.def("getItem",                                                                                                                                                                                                                                      
     &X::getItem,                                                                                                                                                                                                                              
     &X_wrap::default_getItem);

The problem is that the default parameter will not be carried around as part of the method signature.

Boost provides a workaround for this:

BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(getItem_member_overloads, getItem, 0, 1)

It's clear in the non-virtual case how to apply this to the def:

.def("getItem",                                                                                                                                                                                                                                      
     &X::getItem,                                                                                                                                                                                                                              
     getItem_member_overloads());

This compiles and works as expected.

However with the wrapper and the default function to complicate matters when we have a virtual function it's not clear how to combine these. I'm assuming the above cannot be the correct solution as I've removed default_getItem() from the definition.

This lead me to try to create a second set of overloads:

BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(default_getItem_overloads, default_getItem, 0, 1);

The macro compiles, but there doesn't seem to be a way to apply 2 separate sets of overloads to the .def() that doesn't then fail compilation.

Google has suggested I can use boost::python::arg() and define it like arg("a")=1:

Something like the below compiles:

.def("getItem",                                                                                                                                                                                                                                      
     &X::getItem,
     arg("a")=0,                                                                                                                                                                                                                     
     &X_wrap::default_getItem,
     arg("a")=0);

But I get a runtime error:

Boost.Python.ArgumentError: Python argument types in                                                                                                                                                                                                                
    X.getItem(ChildClassOfX)                                                                                                                                                                                                                          
did not match C++ signature:                                                                                                                                                                                                                                        
    getItem(X_wrap {lvalue}, Z* a=0)                                                                                                                                                                                                   
    getItem(X {lvalue}, Z* a=0)

This suggests that ChildClassOfX is for some reason not matching to the signature of getItem() in base class X.

At this point I'm winging it a bit - and my definitions are probably plain and simple wrong!

So far any solution I have either breaks the runtime polymorphism in some way, or doesn't compile.

Any suggestions or examples would be a huge help!

(Note for Pure Virtual Functions the lack of a requirement for a default function means only a single function is passed into the .def() so it looks straightforward to modify the non-virtual trivial example - this is not the case for non-pure virtuals)

EDIT

Found a single reference online to someone else asking the same question - the solution is close to my attempt using args, but doesn't seem to work and goes against the current Boost documentation? It uses the wrapper class in the def() for both getItem() and default_getItem() with a single set of args passed in - below is the example given in the reference. The only other difference is the default value is a value not a pointer as in my case:

def("override", WrapperClass::func, WrapperClass::default_func, (arg("x"), arg("y")=0, arg("z")=false))

Modifying for my example build OK, but throws with:

Boost.Python.ArgumentError: Python argument types in                                                                                                                                                                                                                
    X.getItem(ChildClassOfX)                                                                                                                                                                                                                          
did not match C++ signature:                                                                                                                                                                                                                                        
    getItem(X_wrap {lvalue}, Z* a=0)                                                                                                                                                                                                   
    getItem(X_wrap {lvalue}, Z* a=0)                                                                                                                                                                                                   

Reference: http://boost.2283326.n4.nabble.com/Boost-Python-inheritance-optional-parameters-td4592869.html


Solution

  • I have cobbled together a solution which seems to work, and obey the rules of polymorphism.

    The secret is to simply not use boost::python::args or the BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS macro at all (although I accept that it won't support key-value args in python as-is).

    Simply create an auxillary function that has an l-value matching the class containing the virtual function i.e. X - and has NO other arguments passed into it. This gets rid of the issue of Python not matching the method signature because the parameter a along with it's default value simply do not exist:

    A* getItem_noargs_wrap(X& x)
    {
        return x.getItem();
    }
    

    This seems pointless but it is anything but. The x.getItem() call is C++ to C++ so the default parameter is correctly matched against the empty signature.

    However when we come to write our def() we can now give Python a function that genuinely takes no arguments, allowing it to match getItem() calls in Python.

    The only thing left is to give the compiler a bit of help to know which signature to match to which underlying call:

    .def("getItem",                                                                                                                                                                                                                                      
         &getItem_noargs_wrap);
    .def("getItem",                                                                                                                                                                                                                                      
         &X::getItem,                                                                                                                                                                                                                   
         &X_wrap::default_getItem);
    

    So getItem() is exposed twice to Python - once with no arguments using our auxillary function to call the right instance method behind the scenes, and once taking a Z* and using the standard pattern for non-pure virtual functions.

    The second call is in-effect matching:

    .def<const char* (X::*)(Z*)>("getItem",

    This seems to work well - but I haven't exhaustively tested it doesn't somehow subtely break polymorphism.