Search code examples
c++boost-pythonboost-optional

Exposing boost::optional<T> via Boost.Python as internal reference or None


I am exposing my C++ classes via Boost.Python. My intention is to expose member variables that are of a user-defined class type with an internal reference. This worked fine until I decided to introduce a member variable of type boost::optional<T>.

There are several great posts that show how to expose boost::optional<T> as return by value. Specifically, I have implemented this converter. The other relevant pieces of my code look like this:

struct Bar {}

struct Foo {
  boost::optional<Bar> bar;
}

BOOST_PYTHON_MODULE(mymodule) {
  using namespace boost::python;

  python_optional<Bar>();  //registering the converter

  class_<Foo>("Foo")
    .add_property ( "bar", make_getter(&Foo::bar, return_value_policy<return_by_value>()), make_setter(&Foo::bar) )
  ;
}

I tried to replace return_value_policy<return_by_value>() with either return_value_policy<reference_existing_object>() or return_internal_reference<>(). Both produced a Python TypeError:

>> import mymodule
>> foo = mymodule.Foo()
>> bar = foo.bar
TypeError: No Python class registered for C++ class boost::optional<Bar>

My understanding is that I now I am getting a reference to the boost::optional<T> object. The converter I registered is, however, not called because it expects a boost::optional<T> object and not a reference to such an object. I thought about changing the converter but I am new to this and I really do not know how. Any suggestions?


Solution

  • I found a workaround by adding a getter to struct Foo that either returns a pointer to the object held by boost::optional or a nullptr in case of boost::none. As &*bar returns const Bar* I had to use const_cast.

    struct Bar {}
    
    struct Foo {
      Bar* getBar() { return (bar ? const_cast<Bar*>(&*bar) : nullptr); };
      boost::optional<Bar> bar;
    }
    
    BOOST_PYTHON_MODULE(mymodule) {
      using namespace boost::python;
    
      python_optional<Bar>();  //registering the converter
    
      class_<Foo>("Foo")
        .add_property ( "bar", make_function(static_cast< Bar*(Foo::*)() >(&Foo::getBar), return_internal_reference<>() ), make_setter(&Foo::bar) )
      ;
    }
    

    In case bar belongs to a base class of Foo Python will produce an ArgumentError like this:

    >> import mymodule
    >> foo = mymodule.Foo()
    >> foo.bar = mymodule.Bar()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    Boost.Python.ArgumentError: Python argument tpyes in
      None.None(Foo, Bar)
    did not match C++ signature:
      None(FooBase {lvalue}, boost::optional<Bar>)
    

    To solve the problem define a getter and a setter for bar in class Foo.

    struct Bar {}
    
    struct FooBase {
      boost::optional<Bar> bar;
    }
    
    struct Foo : public FooBase {
      Bar* getBar() { return (bar ? const_cast<Bar*>(&*bar) : nullptr); };
      void setBar(const Bar* bar) { bar ?  this->bar = *bar : this->bar = boost::none; }
    }
    
    void (Foo::*foo_bar_set)(const Bar*) = &Foo::setBar;
    
    BOOST_PYTHON_MODULE(mymodule) {
      using namespace boost::python;
    
      python_optional<Bar>();  //registering the converter
    
      class_<Foo>("Foo")
        .add_property ( "bar", make_function(static_cast< Bar*(Foo::*)() >(&Foo::getBar), return_internal_reference<>() ), foo_bar_set )
      ;
    }