A short TMP experiment with boosts mp11 & fusion

published at 17.03.2018 20:09 by Jens Weller
Save to Instapaper Pocket

Yesterday and today I did some fun coding to play around with boosts new, C++11 TMP library: mp11. I have an idea, and one of the fundamental building blocks would be boost::fusion and its adapt struct infrastructure. I wanted to know if I could combine fusion and mp11 in a good way, to create a certain interface. I'll likely go into the exact ideas and reasoning for this interface later, for now lets focus on how to create it.

I don't write a lot of code with TMP or fusion, so I'm not used to the style. You can see the current version of the code in this gist, which is much cleaner, then what I once started with. Thanks to Peter Dimov for helping with a few improvements, and pointing out that some artifacts were not needed anymore. But I'd like to document the way to this much cleaner code, and - honestly - with out this evolution of my code, I wouldn't understand this code as clear as I do now. So I expect this article will help a few others with that too. Also this blog post will give some of you a first glimpse at mp11.

Setup code

Just quickly going through some code that is part of the setup for the later function.

struct post
{
    int title,desc;
};

BOOST_FUSION_ADAPT_STRUCT(post,title, desc)

A simple, adapted struct. This code makes 'post' a fusion sequence, and makes it later possible to turn the members of post into a mp11 accessible list. As this is just a mock up to test an idea, I chose to make the members of post simple ints. You might guess that these could be strings after their names. Then I have two tag types, just empty structs, which give a hint at where I'd like to go with this in the end: SingleLine and MultiLine.

Originally I planned to go for an intrusive version, where you would declare your members with tag<MemberType, Tags...>, and tag would then behave like MemberType and have a tag interface. But that would cause lots of refactoring in my own code, and exclude all types you simply can't be editing. So tag became tag_view<MemberType&, Tag1, Tags...>, and now I need a function which fuses a set of tags to the members of a fusion adapted struct:

post p{1,2};
auto t2 = tagtype::create_tag_views(p,tagtype::SingleLine{},tagtype::MultiLine{});

create_tag_views returns a tuple<tag_view<Member0,Tag0>, - tag_view<MemberN,TagN>>, which is - as a type - unknown. TMP helps us to "calculate" the correct type and then create the needed tuple. But lets first have a quick view on tag_view it self:

// a template connecting types and tags
template< class TaggedType, typename Tag1, typename... Tags >
class tag_view
{
    TaggedType* value=nullptr;
    using myTags = boost::mp11::mp_list< Tag1,Tags... >;
    public:
    tag_view(){}
    tag_view(TaggedType& val):value(&val){}
    template< class Tag >
    constexpr bool hasTag()const
    {
        return boost::mp11::mp_contains< myTags, Tag>::value;
    }
};

Still a mock up, but this type is there to connect tags to types. The current solution can only add one tag to a type, so there is still some exercise left for the reader ;) But lets move on to the actual TMP.

Calculating the return type with boost::mp11

This is the part of the create_tag_views function which deals with calculating the return type:

template< class seq, class... List >
auto create_tag_views( seq& s,   List&&...  )
{
    std::size_t const N = sizeof...(List);
    using taglist =typename boost::mp11::mp_list< List...>;
    using sequence = typename boost::fusion::traits::deduce_sequence< seq >::type;
    static_assert(N == boost::mp11::mp_size< sequence >::value,"List of tags must be the same size as members in struct");

    using R = typename boost::mp11::mp_append<std::tuple< >, boost::mp11::mp_transform< tag_view, sequence, taglist >>;

So, line by line explained: First the number of arguments in the parameter pack is calculated. Then this parameter pack is unpacked into a mp11 type list (mp_list). Then the first parameter - the fusion adapted struct - is also turned into a fusion sequence which mp11 can handle, so no need to convert this further into a mp_list. The static_assert is catching if the type lists aren't the same size. The code produces a better error message then mp_transform does. Maybe I should previously also test with fusion::is_sequence if the seq type is an adapted struct.

R is the return type, a tuple like previously mentioned. Here mp11 helps us to transform the two lists into each tag_view member of the tuple, and then append it to a growing tuple type with mp_append. But TMP often only works on the types, so while R is the needed type to return, it still needs to be filled with the correct instances from s.

Before I move on to the many ways I tried to fill this tuple, let me mention that this code is now much cleaner then when I started writing it. For some one who is kinda new to TMP, mp11 has a hard learning curve. The documentation is good, but also technical. One always feels tempted to try if it might work with adding ::type or ::value to any mp_type. Also, one always has to keep in mind that this is mostly compile time code. Working with types feels a bit different then the normal day to day C++ code I write with Qt.

Instantiating the tuple

... in three ways. Once there is a tuple, I can look through some of my old fusion codes, to see how to fill in the types into the tuple. This approach - and the second - has one disadvantage: one needs to create the tuple first, and then fill with the correct values. This is also why tag_view has a default constructor, and uses a pointer rather then a reference to store its member. There is a 3rd solution, which constructs the tuple from the values directly, and hence gets around this weakness. The first two solutions work on the R tuple; variable, which is then returned to the caller.

So in fusion, one can traverse a fusion sequence by its index, starting with the last index towards 0. For this one can write a template taking an index, using recursion and specialize it for 0:

template < size_t I>
struct Assign2Tuple
{
    template< class Seq, class tuple >
    static void call(Seq& s, tuple& t)
    {
        using Type = typename std::tuple_element<I,tuple>::type;
        std::get< I >(t) = Type(boost::fusion::get< I >(s));
        Assign2Tuple< I - 1 >::call(s,t);
    }
};

template <>
struct Assign2Tuple< 0 >
{
    template< class Seq, class tuple >
    static void call(Seq& s, tuple& t)
    {
        using Type = typename std::tuple_element<0,tuple>::type;
        std::get< 0 >(t) = Type(boost::fusion::get< 0 >(s));
    }
};

This solution was the first I tried, which I knew would work 100%. I tried to be fancy with mp11::tuple_for_each and a constexpr lambda before that, but gcc 5.3 does not have constexpr lambdas, hence that did not work. Doing it old style, with my own callable type yielded little better results, this is an earlier version of this code on ideone if you want to see this other, not really working solution from me.

The above solution is then called with Assign2Tuple< N-1 >::call(s,tuple); in the template function. How ever I always felt that code like this is ugly, but with my own knowledge, I didn't know a better way.

So the 2nd and 3rd solutions were suggestions from Peter Dimov, the author of mp11. The 2nd solution replaces the above code with this:

boost::mp11::mp_for_each< boost::mp11::mp_iota_c<N> >( 
  [&]( auto I ){ 
    std::get<I>(tuple) = boost::fusion::get<I>(s); 
  } );

This works with C++14, iterates over the mp_list<0,...N> which mp_itoa_c creates, a generic lambda then uses these indexes to access the tuple and fusion sequence to add the correct values to the tuple. Nice solution, but still, first an "empty" tuple with default constructed members is created, and then gets filled with what should be there. What if one could do this directly? I don't want to hope on the optimizer being smart enough to do some magic here...

So it turns out, that is possible with a helper function and std::index_sequence:

template< class R, class Seq, size_t... I>
R tuple_from_seq( Seq& s, std::index_sequence< I... >)
{
return R{ boost::fusion::get< I >(s)... };
}

Which then gets called to return the tuple:

return tuple_from_seq< R >(s,std::make_index_sequence< N >())

Conclusion

And for now, that is the code I wrote. A useless function which calculates a weird tuple containing pointers or references to the members of a fusion adapted struct and tag type. Its an experiment, and was an opportunity to look if this could be done with TMP and mp11. One of the ideas connected to this is, that these tag types can hint other interfaces what is needed for this type to be represented in an UI layer. SingleLine would create a QLineEdit and MultiLine a QTextArea in some backend some where in the future...

 

Join the Meeting C++ patreon community!
This and other posts on Meeting C++ are enabled by my supporters on patreon!