Search code examples
c++dictionaryboostboost-fusion

boost::fusion::map allows duplicate keys


According to the boost::fusion::map docs:

A map may contain at most one element for each key.

In practice, it is easy to violate this.

I am able to define the following type:

using map_type = fusion::map<
    fusion::pair<int, char>
  , fusion::pair<int, char>
  , fusion::pair<int, char>>;

and instantiate it with these duplicate keys:

map_type m(
    fusion::make_pair<int>('X')
  , fusion::make_pair<int>('Y')
  , fusion::make_pair<int>('Z'));

Iterating over the map using fusion::for_each shows the data structure does indeed contain 3 pairs, and each of the keys is of type int:

struct Foo
{
    template<typename Pair>
    void operator()(const Pair& p) const
    {
        std::cout << typeid(typename Pair::first_type).name() << "=" << p.second << '\n';
    }
};
fusion::for_each(m, Foo {});

Output:

i=X
i=Y
i=Z

I would have expected a static_assert on key uniqueness, but this is obviously not the case.

  • Why is this?

  • How can I ensure that no one can instantiate a fusion::map with duplicate keys?

Full working example: (on coliru)

#include <boost/fusion/container.hpp>
#include <boost/fusion/include/for_each.hpp>
#include <iostream>

namespace fusion = ::boost::fusion;

struct Foo
{
    template<typename Pair>
    void operator()(const Pair& p) const
    {
        std::cout << typeid(typename Pair::first_type).name() << "=" << p.second << '\n';
    }
};

int main()
{
    using map_type = fusion::map<
        fusion::pair<int, char>
      , fusion::pair<int, char>
      , fusion::pair<int, char>>;

    map_type m(
        fusion::make_pair<int>('X')
      , fusion::make_pair<int>('Y')
      , fusion::make_pair<int>('Z'));

    fusion::for_each(m, Foo {});
    return 0;
}

Due to comments below, here are some further details on what I'm actually trying to achieve.

The idea is to automatically generate FIX serialisation code.

A given field type can only exist once in any given FIX message - hence wanting the static_assert

Motivating example: (on coliru)

#include <boost/fusion/container.hpp>
#include <boost/fusion/sequence.hpp>
#include <boost/fusion/include/for_each.hpp>
#include <boost/mpl/transform.hpp>
#include <iostream>

namespace fusion = ::boost::fusion;
namespace mpl    = ::boost::mpl;

template<class Field>
struct MakePair
{
    using type = typename fusion::result_of::make_pair<Field, typename Field::Type>::type;
};

template<class Fields>
struct Map
{
    using pair_sequence = typename mpl::transform<Fields, MakePair<mpl::_1>>::type;
    using type          = typename fusion::result_of::as_map<pair_sequence>::type;
};

///////////////////////////

template<typename... Fields>
class Message
{
public:
    template<class Field>
    void set(const typename Field::Type& val)
    {
        fusion::at_key<Field>(_fields) = val;
    }

    void serialise()
    {
        fusion::for_each(_fields, Serialiser {});
    }
private:

    struct Serialiser
    {
        template<typename Pair>
        void operator()(const Pair& pair) const
        {
            using Field = typename Pair::first_type;

            std::cout << Field::Tag << "=" << pair.second << "|";
        }
    };

    using FieldsVector = fusion::vector<Fields...>;
    using FieldsMap    = typename Map<FieldsVector>::type;

    FieldsMap _fields;

    static_assert(fusion::result_of::size<FieldsMap>::value == fusion::result_of::size<FieldsVector>::value,
            "message must be constructed from unique types"); // this assertion doesn't work
};

///////////////////////////

#define MSG_FIELD(NAME, TYPE, TAG)  \
    struct NAME                     \
    {                               \
        using Type = TYPE;          \
        static const int Tag = TAG; \
    };

MSG_FIELD(MsgType, char,   35)
MSG_FIELD(Qty,     int,    14)
MSG_FIELD(Price,   double, 44)

using Quote = Message<MsgType, Qty, Price>;

///////////////////////////

int main()
{
    Quote q;
    q.set<MsgType>('a');
    q.set<Qty>(5);
    q.set<Price>(1.23);

    q.serialise();
    return 0;
}

Solution

  • From the docs on associative containers:

    ... Keys are not checked for uniqueness.

    As alluded to by Richard Hodges, this is likely by design

    wouldn't that static_assert involve a geometric template expansion each time it was encountered?

    Nonetheless, it is possible to use boost::mpl to reduce the sequence provided to the fusion::map into a unique sequence, and static_assert on the sequence lengths being the same.

    First we create a struct which iterates over the list of types and creates a sequence of unique types

    // given a sequence, returns a new sequence with no duplicates
    // equivalent to:
    //  vector UniqueSeq(vector Seq)
    //      vector newSeq = {}
    //      set uniqueElems = {}
    //      for (elem : Seq)
    //          if (!uniqueElems.find(elem))
    //              newSeq += elem
    //              uniqueElems += elem
    //      return newSeq
    template<class Seq>
    struct UniqueSeq
    {
        using type = typename mpl::accumulate<
            Seq,
            mpl::pair<typename mpl::clear<Seq>::type, mpl::set0<> >,
            mpl::if_<
                mpl::contains<mpl::second<mpl::_1>, mpl::_2>,
                mpl::_1,
                mpl::pair<
                    mpl::push_back<mpl::first<mpl::_1>, mpl::_2>,
                    mpl::insert<mpl::second<mpl::_1>, mpl::_2>
                >
            >
        >::type::first;
    };
    

    Then we change the definition of Map to use UniqueSeq::type to generate pair_sequence:

    // given a sequence of fields, returns a fusion map which maps (Field -> Field's associate type)
    template<class Fields>
    struct Map
    {
        using unique_fields = typename UniqueSeq<Fields>::type;
        using pair_sequence = typename mpl::transform<unique_fields, MakePair<mpl::_1>>::type;
        using type          = typename fusion::result_of::as_map<pair_sequence>::type;
    };
    

    So given a list of fields, we can create a fusion::vector and a fusion::map with the result of UniqueSeq<Fields>, and assert the size of each is the same:

    using FieldsVector = fusion::vector<Fields...>;
    using FieldsMap    = typename Map<FieldsVector>::type;
    
    static_assert(fusion::result_of::size<FieldsMap>::value == fusion::result_of::size<FieldsVector>::value,
            "message must be constructed from unique types");
    

    Passing duplicated fields now causes a compilation error:

    static assertion failed: message must be constructed from unique types

    scratch/main.cpp: In instantiation of ‘class Message<Qty, Price, Qty>’:
    scratch/main.cpp:129:23:   required from here
    scratch/main.cpp:96:5: error: static assertion failed: message must be constructed from unique types
         static_assert(fusion::result_of::size<FieldsMap>::value == fusion::result_of::size<FieldsVector>::value,
         ^
    

    Full example on coliru