14
\$\begingroup\$

I could not find a Boost Python converter which converts std::tuple, so I wrote one.

This was tested with a g++ 4.7 snapshot on Debian squeeze. It uses C++ 11 features, specifically variadic templates, so requires a recent compiler. This is based on the answer to ""unpacking" a tuple to call a matching function pointer".

It is rather long, and could probably be improved/simplified. In particular, the two *_wrapper structures look redundant. How do I replace the cpptuple2pytuple_wrapper and pytuple2cpptuple_wrapper with one struct, and perhaps template on the differences using something like type traits, if that is possible? Any other simplifications would also be interesting.

cpptup_conv_pif.cpp

#include <tuple>
#include <string>
#include <iostream>
using std::cout;
using std::endl;
using std::string;

#include <boost/python/tuple.hpp>
#include <boost/python/extract.hpp>
#include <boost/python/object.hpp>
#include <boost/python/module.hpp>
#include <boost/python/class.hpp>
#include <boost/python/def.hpp>
using boost::python::extract;

template<int ...> struct seq{};
template<int N, int ...S> struct gens : gens<N-1, N-1, S...>{};
template<int ...S> struct gens<0, S...> {typedef seq<S...> type;};

template <typename ...Args>
struct cpptuple2pytuple_wrapper
{
  std::tuple<Args...> params;
  cpptuple2pytuple_wrapper(const std::tuple<Args...>& _params):params(_params){}

  boost::python::tuple delayed_dispatch()
  {
    return callFunc(typename gens<sizeof...(Args)>::type());
  }

  template<int ...S>
  boost::python::tuple callFunc(seq<S...>)
  {
    return boost::python::make_tuple(std::get<S>(params) ...);
  }
};

template <typename ...Args>
struct pytuple2cpptuple_wrapper
{
  boost::python::tuple params;
  pytuple2cpptuple_wrapper(const boost::python::tuple& _params):params(_params){}

  std::tuple<Args...> delayed_dispatch()
  {
    return callFunc(typename gens<sizeof...(Args)>::type());
  }

  template<int ...S>
  std::tuple<Args...> callFunc(seq<S...>)
  {
    return std::make_tuple((static_cast<Args>(extract<Args>(params[S])))...);
  }
};

// Convert (C++) tuple to (Python) tuple as PyObject*.
template<typename ... Args> PyObject* cpptuple2pytuple(const std::tuple<Args...>& t)
{
  cpptuple2pytuple_wrapper<Args...> wrapper(t);
  boost::python::tuple bpt = wrapper.delayed_dispatch();
  return boost::python::incref(boost::python::object(bpt).ptr());
}

// Convert (Python) tuple to (C++) tuple.
template<typename ... Args> std::tuple<Args...> pytuple2cpptuple(PyObject* obj)
{
  boost::python::tuple tup(boost::python::borrowed(obj));
  pytuple2cpptuple_wrapper<Args...> wrapper(tup);
  std::tuple<Args...> bpt = wrapper.delayed_dispatch();
  return bpt;
}

template<typename ... Args>
struct cpptuple_to_python_tuple
{
  static PyObject* convert(const std::tuple<Args...>& t)
    {
      return cpptuple2pytuple<Args...>(t);
    }
};

template<typename ... Args>
struct cpptuple_from_python_tuple
{
  cpptuple_from_python_tuple()
  {
    boost::python::converter::registry::push_back(
                          &convertible,
                          &construct,
                          boost::python::type_id<std::tuple<Args...> >());
  }

  static void* convertible(PyObject* obj_ptr)
  {
    if (!PyTuple_CheckExact(obj_ptr)) return 0;
    return obj_ptr;
  }

  static void construct(
            PyObject* obj_ptr,
            boost::python::converter::rvalue_from_python_stage1_data* data)
  {
    void* storage = (
             (boost::python::converter::rvalue_from_python_storage<std::tuple<Args...> >*)
             data)->storage.bytes;
    new (storage) std::tuple<Args...>(pytuple2cpptuple<Args...>(obj_ptr));
    data->convertible = storage;
  }
};

template<typename ...Args> void create_tuple_converter()
{
  boost::python::to_python_converter<std::tuple<Args...>, cpptuple_to_python_tuple<Args...> >();
  cpptuple_from_python_tuple<Args...>();
}

void export_cpptuple_conv()
{
  create_tuple_converter<int, float>();
  create_tuple_converter<int, double, string>();
}

std::tuple<int, float> tupid1(std::tuple<int, float> t){return t;}
std::tuple<int, double, string> tupid2(std::tuple<int, double, string> t){return t;}

BOOST_PYTHON_MODULE(bptuple)
{
  export_cpptuple_conv();
  boost::python::def("tupid1", tupid1);
  boost::python::def("tupid2", tupid2);
}

This can be compiled with the following SConstruct file.

SConstruct

#!/usr/bin/python
import commands, glob, os

def pyversion():
        pystr = commands.getoutput('python -V')
        version = pystr.split(' ')[1]
        major, minor = version.split('.')[:2]
        return major + '.' + minor

boost_python_env = Environment(
        #CXX="g++",
        CXX="g++-4.7",
        CPPPATH=["/usr/include/python"+pyversion()],
        CXXFLAGS="-ftemplate-depth-100 -fno-strict-aliasing -ansi -Wextra -Wall -Werror -Wno-unused-function -g -O3 -std=c++11",
        CPPDEFINES=['BOOST_PYTHON_DYNAMIC_LIB'],
        LIBPATH=["/usr/lib/python"+pyversion()+"/config"],
        LIBS=["python"+pyversion(), "m", "boost_python"],
        SHLIBPREFIX="", #gets rid of lib prefix
        )

boost_python_env.SharedLibrary(target="bptuple", source=["cpptup_conv_pif.cpp"])

The compilation looks like this on my machine:

g++-4.7 -o cpptup_conv_pif.os -c -ftemplate-depth-100 -fno-strict-aliasing -ansi -Wextra -Wall -Werror -Wno-unused-function -g -O3 -std=c++11 -fPIC -DBOOST_PYTHON_DYNAMIC_LIB -I/usr/include/python2.6 cpptup_conv_pif.cpp
g++-4.7 -o bptuple.so -shared cpptup_conv_pif.os -L/usr/lib/python2.6/config -lblitz -lRmath -lpython2.6 -lm -lboost_python

Usage:

In [1]: import bptuple

In [2]: bptuple.tupid1((1,1.2))
Out[2]: (1, 1.2000000476837158)

In [3]: bptuple.tupid2((1,1.2, "foo"))
Out[3]: (1, 1.2, 'foo')
\$\endgroup\$
0

2 Answers 2

3
+100
\$\begingroup\$

One thing that's not immediately clear to me is if the tuple2tuple_wrapper's are supposed to be internal helpers, or actually part of the interface. They seem to be instantiated only once each in the code shown, both immediately followed by a call to their delayed_dispatch.

If these are only internal helpers, you could trim them down a lot. You'll still need a templated struct with a templated member function, since you have two template parameter packs. But you're not actually using delayed_dispatch as it's name suggests, so that can go. And then the params member and ctor can go as well, with params becoming an extra parameter to callFunc. Taking cpptuple2pytuple_wrapper as an example:

template<typename... Args>
struct cpp_to_py_wrapper { // I suck at typing typle
    template<int... S>
    boost::python::tuple callFunc(std::tuple<Args...> params, seq<S...>) {
        return boost::python::make_tuple(std::get<S>(params)...);
    }
};

cpptuple2pytuple then becomes:

template<typename ... Args> PyObject* cpptuple2pytuple(const std::tuple<Args...>& t)
{
    boost::python::tuple bpt = cpp_to_py_wrapper<Args...>().callFunc(t, typename gens<sizeof...(Args)>::type());
    return boost::python::incref(boost::python::object(bpt).ptr());
}

Now you're just left with two instances of callFunc, both of which show a similar composition. If we just abstract for a bit:

template<int ...S>
return_type callFunc(params_type params, seq<S...>)
{
    return construct_tuple<return_type>(extract_tuple_value<S, Args>(params) ...);
}

Now, this is definitely something that can be implemented generically. However, if you're only using this here, I'd actually advise against it. Since you're only defining two conversions, hardcoding both overloads of callFunc will likely be less work than providing suitable implementations of construct_tuple and extract_tuple_value. A generic implementation will likely not pay off until you want to convert between more than three types. That's the point were the number of pair-wise combinations of N elements starts to grow faster than N itself.

\$\endgroup\$
5
  • \$\begingroup\$ Thanks for the answer, Daan. Unfortunately, I don't remember this question that well - it has been a while. Working code would be helpful. I'm not sure what you mean by typle2tuple - there is no such name in my code. \$\endgroup\$ Commented Dec 29, 2014 at 16:29
  • \$\begingroup\$ That should have been tuple. I was referring to cpptuple2pytuple_wrapper and it's counterpart. As for the working code: like I said, I don't have a compatible build environment. So unless you can provide a self-contained drop of your code, I can't even test what I've written so far ;-) \$\endgroup\$
    – Daan
    Commented Dec 29, 2014 at 16:35
  • \$\begingroup\$ Define self-contained drop. I think if you cut and paste the code it will probably work. And I'm not sure what you mean by "compatible build environment". Can you elaborate? \$\endgroup\$ Commented Dec 29, 2014 at 16:54
  • \$\begingroup\$ Yes, all that is rather central to testing the example. :-) What is your OS/distribution? \$\endgroup\$ Commented Dec 29, 2014 at 17:29
  • \$\begingroup\$ I use Debian wheezy. It's trivial to get all that stuff on Ubuntu. Try libboost-python-dev, scons, python-all-dev, g++. That would pull in most of it. \$\endgroup\$ Commented Dec 29, 2014 at 18:33
1
\$\begingroup\$

There is a way to simplify this by using only a single wrapper template. First, here's the single template:

template <typename P, typename R, typename ...Args>
struct tupleconvert_wrapper
{
  P params;
  tupleconvert_wrapper(const P& _params):params(_params){}

  R delayed_dispatch()
  {
    return callFunc(typename gens<sizeof...(Args)>::type());
  }

  template<int ...S, typename = typename std::enable_if<(sizeof(S),std::is_same<boost::python::tuple, R>::value), R>::type>
  boost::python::tuple callFunc(seq<S...>)
  {
    return boost::python::make_tuple(std::get<S>(params) ...);
  }

  template<int ...S, typename = typename std::enable_if<(sizeof(S),std::is_same<boost::python::tuple, P>::value), R>::type>
  std::tuple<Args...> callFunc(seq<S...>)
  {
    return std::make_tuple((static_cast<Args>(extract<Args>(params[S])))...);
  }
};

Here are the uses of it, as converted from your original code.

// Convert (C++) tuple to (Python) tuple as PyObject*.
template<typename ... Args> PyObject* cpptuple2pytuple(const std::tuple<Args...>& t)
{
  tupleconvert_wrapper<std::tuple<Args...>, boost::python::tuple, Args...> wrapper(t);
  boost::python::tuple bpt = wrapper.delayed_dispatch();
  return boost::python::incref(boost::python::object(bpt).ptr());
}

// Convert (Python) tuple to (C++) tuple.
template<typename ... Args> std::tuple<Args...> pytuple2cpptuple(PyObject* obj)
{
  boost::python::tuple tup(boost::python::borrowed(obj));
  tupleconvert_wrapper<boost::python::tuple, std::tuple<Args...>, Args...> wrapper(tup);
  std::tuple<Args...> bpt = wrapper.delayed_dispatch();
  return bpt;
}

You may be wondering why the sizeof(S) is part of the std::enable_if clause. It's needed to force the enable_if to be dependent on the template parameter. Otherwise your compiler will issue an error as in this question.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.