Our goal is to make something like the following SWIG interface work intuitively:
%module test
%include "std_tuple.i"
%std_tuple(TupleDD, double, double);
%inline %{
   std::tuple<double, double> func() {
       return std::make_tuple(0.0, 1.0);
   }
%}
We want to use this within Python in the following way:
import test
r=test.func()
print(r)
print(dir(r))
r[1]=1234
for x in r:
    print(x)
i.e. indexing and iteration should just work.
By re-using some of the pre-processor tricks I used to wrap std::function (which were themselves originally from another answer here on SO) we can define a neat macro that "just wraps" std::tuple for us. Although this answer is Python specific it should in practice be fairly simple to adapt for most other languages too. I'll post my std_tuple.i file, first and then annotate/explain it after:
// [1]
%{
#include <tuple>
#include <utility>
%}
// [2]    
#define make_getter(pos, type) const type& get##pos() const { return std::get<pos>(*$self); }
#define make_setter(pos, type) void set##pos(const type& val) { std::get<pos>(*$self) = val; }
#define make_ctorargN(pos, type) , type v##pos
#define make_ctorarg(first, ...) const first& v0 FOR_EACH(make_ctorargN, __VA_ARGS__)
// [3]
#define FE_0(...)
#define FE_1(action,a1) action(0,a1)
#define FE_2(action,a1,a2) action(0,a1) action(1,a2)
#define FE_3(action,a1,a2,a3) action(0,a1) action(1,a2) action(2,a3)
#define FE_4(action,a1,a2,a3,a4) action(0,a1) action(1,a2) action(2,a3) action(3,a4)
#define FE_5(action,a1,a2,a3,a4,a5) action(0,a1) action(1,a2) action(2,a3) action(3,a4) action(4,a5)
#define GET_MACRO(_1,_2,_3,_4,_5,NAME,...) NAME
%define FOR_EACH(action,...)
  GET_MACRO(__VA_ARGS__, FE_5, FE_4, FE_3, FE_2, FE_1, FE_0)(action,__VA_ARGS__)
%enddef
// [4]
%define %std_tuple(Name, ...)
%rename(Name) std::tuple<__VA_ARGS__>;
namespace std {
    struct tuple<__VA_ARGS__> {
        // [5]
        tuple(make_ctorarg(__VA_ARGS__));
        %extend {
            // [6]
            FOR_EACH(make_getter, __VA_ARGS__)
            FOR_EACH(make_setter, __VA_ARGS__)
            size_t __len__() const { return std::tuple_size<std::decay_t<decltype(*$self)>>{}; }
            %pythoncode %{
                # [7]
                def __getitem__(self, n):
                    if n >= len(self): raise IndexError()
                    return getattr(self, 'get%d' % n)()
                def __setitem__(self, n, val):
                    if n >= len(self): raise IndexError()
                    getattr(self, 'set%d' % n)(val)
            %}
        }
    };
}
%enddef
- This is just the extra includes we need for our macro to work
- These apply to each of the type arguments we supply to our %std_tuplemacro invocation, we need to be careful with commas here to keep the syntax correct.
- This is the mechanics of our FOR_EACHmacro, which invokes each action per argument in our variadic macro argument list
- Finally the definition of %std_tuplecan begin. Essentially this is manually doing the work of%templatefor each specialisation ofstd::tuplewe care to name inside of the std namespace.
- We use our macro for each magic to declare a constructor with arguments for each element of the correct type. The actual implementation here is the default one from the C++ library which is exactly what we need/want though.
- We use our FOR_EACHmacro twice to make a member functionget0,get1,getNof the correct type of each tuple element and the correct number of them for the template argument size. Likewise forsetN. Doing it this way allows the usual SWIG typemaps fordouble, etc. or whatever types your tuple contains to be applied automatically and correctly for each call tostd::get<N>. These are really just an implementation detail, not intended to be part of the public interface, but exposing them makes no real odds.
- Finally we need an implementation of __getitem__and a corresponding__setitem__. These simply look up and call the rightgetN/setNfunction on the class and call that instead. We take care to raiseIndexErrorinstead of the default exception if an invalid index is used as this will stop iteration correctly when we try to iterate of the tuple.
This is then sufficient that we can run our target code and get the following output:
$ swig3.0 -python -c++ -Wall test.i && g++ -shared -o _test.so test_wrap.cxx -I/usr/include/python3.7 -m32 && python3.7 run.py
<test.TupleDD; proxy of <Swig Object of type 'std::tuple< double,double > *' at 0xf766a260> >
['__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__len__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', '__swig_destroy__', '__swig_getmethods__', '__swig_setmethods__', '__weakref__', 'get0', 'get1', 'set0', 'set1', 'this']
0.0
1234.0
Generally this should work as you'd hope in most input/output situations in Python.
There are a few improvements we could look to make:
- Implement repr
- Implement slicing so that tuple[n:m]type indexing works
- Handle unpacking like Python tuples.
- Maybe do some more automatic conversions for compatible types?
- Avoid calling __len__for every get/setitem call, either by caching the value in the class itself, or postponing it until the method lookup fails?