Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to wrap a C++ function that returns boost::optional<T>?

I want to wrap a function that returns a boost::optional<T>. That is, given:

class Foo {
    boost::optional<T> func();
};

I'd like to wrap that somehow so that Python either gets a T by value, or None:

class_<Foo>("Foo")
    .def("func", func, return_value_policy<return_boost_optional???>);

Normally if it just returned a T, I could use:

class_<Foo>("Foo")
    .def("func", func, return_value_policy<return_by_value>());

But since it returns a boost::optional<T>, it could also return boost::none, which I'd like to end up as Python's None.

Is there a way to do this with the existing converters? If not, is there some workaround to achieve the same effect?

like image 238
congusbongus Avatar asked Oct 21 '14 23:10

congusbongus


1 Answers

The ResultConverter concept is designed to solve this problem. The return_value_policy CallPolicy model uses a ResultConverterGenerator to create a ResultConverter, and the ResultConverter is used to modify the return value of a function being exposed to Python. In this case, a custom type that implements the ResultConverter concept could be used to either return Python None or instantiate an object with the appropriate Python class. While the documentation list all the type requirements, it may be easier to understood with something closer resembling code:

/// @brief The ResultConverterGenerator.
struct result_converter_generator
{
  template <typename T>
  struct apply
  {
    struct result_converter
    {
      // Must be default constructible.
      result_converter();

      // Return true if T can be converted to a Python Object.
      bool convertible();

      // Convert obj to a PyObject, explicitly managing proper reference
      // counts.
      PyObject* operator(const T& obj);

      // Returns the Python object that represents the type.  Used for
      // documentation.
      const PyTypeObject* get_pytype();
    };

    /// @brief The ResultConverter.
    typedef result_converter type;
  };
};

Often times, when creating a custom ResultConverter model, one can use template meta-programming to minimize the chances of runtime errors in conversions, and even catch errors at compile time and provide meaningful messages. Here is a complete example of return_optional, a ResultConverter model that takes a C++ boost::optional<T> object, and converts it to the appropriate Python object.

#include <boost/mpl/if.hpp>
#include <boost/optional.hpp>
#include <boost/python.hpp>
#include <boost/type_traits/integral_constant.hpp>
#include <boost/utility/in_place_factory.hpp>

/// @brief Mockup model.
class spam {};

/// @brief Mockup factory for model.
boost::optional<spam> make_spam(bool x)
{
  return x ? boost::optional<spam>(boost::in_place()) : boost::none;
}

namespace detail {

/// @brief Type trait that determines if the provided type is
///        a boost::optional.
template <typename T>
struct is_optional : boost::false_type {};

template <typename T>
struct is_optional<boost::optional<T> > : boost::true_type {};

/// @brief Type used to provide meaningful compiler errors.
template <typename>
struct return_optional_requires_a_optional_return_type {};

/// @brief ResultConverter model that converts a boost::optional object to
///        Python None if the object is empty (i.e. boost::none) or defers
///        to Boost.Python to convert object to a Python object.
template <typename T>
struct to_python_optional
{
  /// @brief Only supports converting Boost.Optional types.
  /// @note This is checked at runtime.
  bool convertible() const { return detail::is_optional<T>::value; }

  /// @brief Convert boost::optional object to Python None or a
  ///        Boost.Python object.
  PyObject* operator()(const T& obj) const
  {
    namespace python = boost::python;
    python::object result =
      obj                      // If boost::optional has a value, then
        ? python::object(*obj) // defer to Boost.Python converter.
        : python::object();    // Otherwise, return Python None.

    // The python::object contains a handle which functions as
    // smart-pointer to the underlying PyObject.  As it will go
    // out of scope, explicitly increment the PyObject's reference
    // count, as the caller expects a non-borrowed (i.e. owned) reference.
    return python::incref(result.ptr());
  }

  /// @brief Used for documentation.
  const PyTypeObject* get_pytype() const { return 0; }
};

} // namespace detail

/// @brief Converts a boost::optional to Python None if the object is
///        equal to boost::none.  Otherwise, defers to the registered
///        type converter to returs a Boost.Python object.
struct return_optional 
{
  template <class T> struct apply
  {
    // The to_python_optional ResultConverter only checks if T is convertible
    // at runtime.  However, the following MPL branch cause a compile time
    // error if T is not a boost::optional by providing a type that is not a
    // ResultConverter model.
    typedef typename boost::mpl::if_<
      detail::is_optional<T>,
      detail::to_python_optional<T>,
      detail::return_optional_requires_a_optional_return_type<T>
    >::type type;
  }; // apply
};   // return_optional

BOOST_PYTHON_MODULE(example)
{
  namespace python = boost::python;
  python::class_<spam>("Spam")
    ;

  python::def("make_spam", &make_spam,
    python::return_value_policy<return_optional>());
}

Interactive usage:

>>> import example
>>> assert(isinstance(example.make_spam(True), example.Spam))
>>> assert(example.make_spam(False) is None)

The compile time type checking occurs when the return_optional ResultConvert is attempted to be used with a function that returns a value that is not a boost::optional. For instance, when the following is used:

struct egg {};

egg* make_egg();

BOOST_PYTHON_MODULE(example)
{
  namespace python = boost::python;
  python::def("make_egg", &make_egg,
    python::return_value_policy<return_optional>());
}

The compiler will select select the return_optional_requires_a_optional_return_type implementation, and fail compilation. Here is part of the compiler error message clang provides:

error: no member named 'get_pytype' in
'detail::return_optional_requires_a_optional_return_type<egg *>'
like image 123
Tanner Sansbury Avatar answered Sep 30 '22 14:09

Tanner Sansbury