c++boostc++17boost-multi-index

Storing references/pointers to Boost MultiSet indexes


I'm trying to build two classes Collection and CollectionView, which is an abstraction on top of Boost.MultiIndex. The idea is somewhat simple: Give an instance of type Collection to a CollectionView, and it'll handle rendering. The Collection adds an interface for adding or removing items, which in turn will signal to the CollectionView that it needs to do some work.

An excerpt of the classes looks like this:

template<class ItemType, typename ... Indexes>
struct Collection {
    using t_container = multi_index_container<ItemType, indexed_by<Indexes...>>;
    t_container itemSet;
}


template<class TCollectionType>
struct CollectionView {
    // Imagine that this view also keeps track of which
    // item in col corresponds to which item on the screen
    TCollectionType& col;
}

The idea is that the user of the API is in full control over what columns can be indexed, and that this is validated as much as possible for correctness during compile time.

But if we imagine for a moment that a CollectionView has a UI to let the user determine the sorting (from a pre-defined set, since it has to be known at compile time), the way to achieve that becomes slightly problematic.

  1. The CollectionView would ideally iterate through the collection, by knowing how to get to the correct index
  2. The iteration index may change as a response to user input

I somehow have to store what the current iteration index is, and indeed also access this from the Collection's itemSet.

Ideally I would add the following property to Collection:

struct CollectionView {
    t_index_type    currentIndex { col.itemSet.get<0>() }
    
    void Build() {
        for (const auto& item : currentIndex) {
            // Do stuff here
        }
    }
}

I'm failing to figure out what, if any, t_index_type could be, since every index (sequenced, ordered_non_unique, etc..) has its own type. I could impose on the user to implement a void Iterate(function<void(const ItemType&>), but that imposes much more code on the user of the API.

Have I reached a dead end here, or is my templating-fu just not good enough?

EDIT:

One possible solution would be to use templating like so:

// Previous definitions omitted
struct Collection {
    using t_callback_fn = std::function<void(const ItemType&)>;
    using t_iter_fn = std::function<void(t_callback_fn)>;
    void Iterate(t_callback_fn cb) const
    {
        iterFn(cb);
    }
    
    template<int N, bool reverse = false>
    void SetSortIndex()
    {
        iterFn = [this](t_callback_fn fn) {
            // The ideal would be to store this index as part of the class itself!
            auto& index = itemSet.template get<N>();
            if (reverse) {
                for (auto it { index.rbegin() }; it != index.rend(); ++it)
                    fn(*it);
                
            } else {
                for (const auto &item : index)
                    fn(item);
            }
        };
    }
}

And use the container:

col.SetSortIndex<0, true>;
col.Iterate([](const auto& it) { std::cout << it << '\n;};

But it's not quite great.


Solution

  • It sounds like you would really be served by just the random_access_index from Boost Multi Index (BMI).

    You can rearrange it in any way you wish. So even if you want to have the user manually rearrange things e.g. they

    then you can.

    As an aside: note that you can also use BMI containers to merely index non-owned or shared elements. The implementation allows the element type to be T*, T const*, std::reference_wrapper, shared_ptr etc. without any other change to the functionality. Note that it uses generic pointer_traits for this so you can even use std::reference_wrapper<std::shared_ptr<T const*> > and it would still work.

    This is not related to the answer but does resonate with the concept of "external views" as you were contemplating.

    See e.g. https://www.boost.org/doc/libs/1_73_0/libs/multi_index/doc/reference/key_extraction.html#chained_pointers

    DEMONSTRATION

    Let's say we add a random_access index transparently into your container:

    template<class ItemType, typename ... Indexes>
    class Collection {
        template <typename> friend struct CollectionView;
        struct View;
        using t_container = bmi::multi_index_container<ItemType, 
          bmi::indexed_by<
            Indexes...,
            bmi::random_access<bmi::tag<View> > // additional!
          >
      >;
    
      private:
        t_container itemSet;
    };
    

    Now we can define the view to basically work on that extra index:

    template<class TCollectionType>
    struct CollectionView {
        using MIC   = typename TCollectionType::t_container;
        using Tag   = typename TCollectionType::View;
        using Index = typename MIC::template index<Tag>::type;
    
        TCollectionType& col;
        Index& idx { col.itemSet.template get<Tag>() };
    
        // Imagine that this view also keeps track of which
        // item in col corresponds to which item on the screen
        //
        explicit CollectionView(TCollectionType& col) : col(col) {}
    
        auto begin() const { return idx.begin(); }
        auto end() const { return idx.end(); }
    };
    

    Now, I'll add some arranging functions, both arranging by some existing index:

    template <int n> void arrange_by() {
        idx.rearrange(col.itemSet.template get<n>().begin());
    }
    

    Or arranging by a free user-specified comparison function:

    template <typename Cmp> void arrange_by(Cmp cmp) {
        std::vector<std::reference_wrapper<T const> > v(idx.begin(), idx.end());
        std::sort(v.begin(), v.end(), cmp);
        idx.rearrange(v.begin());
    }
    

    Live On Coliru

    #include <boost/multi_index_container.hpp>
    #include <boost/multi_index/ordered_index.hpp>
    #include <boost/multi_index/random_access_index.hpp>
    #include <boost/multi_index/member.hpp>
    #include <iostream>
    #include <iomanip>
    
    namespace bmi = boost::multi_index;
    
    template<class ItemType, typename ... Indexes>
    class Collection {
        template <typename> friend struct CollectionView;
        struct View;
        using t_container = bmi::multi_index_container<ItemType, 
              bmi::indexed_by<
                  Indexes...,
                  bmi::random_access<bmi::tag<View> > // additional!
              >
           >;
    
      public:
        explicit Collection(std::initializer_list<ItemType> init) : itemSet(init) {}
    
        bool insert(ItemType const& item) {
            return itemSet.insert(item).second;
        }
    
        template <int index = 0, typename K>
        bool erase(K const& key) {
            return itemSet.template get<index>().erase(key);
        }
      private:
        t_container itemSet;
    };
    
    template<class TCollectionType>
    struct CollectionView {
        using MIC   = typename TCollectionType::t_container;
        using T     = typename MIC::value_type;
        using Tag   = typename TCollectionType::View;
        using Index = typename MIC::template index<Tag>::type;
    
        TCollectionType& col;
        Index& idx { col.itemSet.template get<Tag>() };
    
        // Imagine that this view also keeps track of which
        // item in col corresponds to which item on the screen
        //
        explicit CollectionView(TCollectionType& col) : col(col) {}
    
        template <int n> void arrange_by() {
            idx.rearrange(col.itemSet.template get<n>().begin());
        }
    
        template <typename Cmp> void arrange_by(Cmp cmp) {
            std::vector<std::reference_wrapper<T const> > v(idx.begin(), idx.end());
            std::stable_sort(v.begin(), v.end(), cmp);
            idx.rearrange(v.begin());
        }
    
        auto begin() const { return idx.begin(); }
        auto end() const { return idx.end(); }
    };
    
    /// example application
    struct Item {
        int id;
        std::string name;
    
        // some natural ordering just for demo
        bool operator<(Item const& other) const 
            { return std::tie(id, name) < std::tie(other.id, other.name); }
        bool operator>(Item const& other) const 
            { return std::tie(id, name) > std::tie(other.id, other.name); }
    };
    
    using Items = Collection<Item,
          bmi::ordered_unique<bmi::member<Item, int, &Item::id> >,
          bmi::ordered_unique<bmi::member<Item, std::string, &Item::name> > >;
    
    int main() {
        Items items {
            { 3, "three" },
            { 1, "one" },
            { 5, "five" },
            { 4, "four" },
            { 2, "two" },
            { 6, "six" },
        };
    
        CollectionView view(items);
    
        auto dump = [&view](auto caption) {
            std::cout << std::setw(12) << caption << ": ";
            for (auto const& [id, name] : view)
                std::cout << " { " << id << ", " << std::quoted(name) << " }";
            std::cout << "\n";
        };
    
        dump("default");
    
        view.arrange_by<1>(); // by name
        dump("by name");
    
        view.arrange_by<0>(); // by id
        dump("by id");
    
        view.arrange_by(std::less<Item>{});
        dump("std::less");
    
        view.arrange_by(std::greater<Item>{});
        dump("std::greater");
    
        auto funky = [](Item const& a, Item const& b) {
            return (a.name.length() - a.id) < (b.name.length() - b.id);
        };
        view.arrange_by(funky);
        dump("funky");
    
        // mutations are fine
        if (items.erase(1))
            std::cout << "Removed 1\n";
        dump("funky");
    
        if (items.insert(Item { 42, "answer" }))
            std::cout << "Inserted the answer (appears at end)\n";
        dump("funky");
    
        view.arrange_by<1>();
        dump("by name");
    }
    

    Prints

         default:  { 3, "three" } { 1, "one" } { 5, "five" } { 4, "four" } { 2, "two" } { 6, "six" }
         by name:  { 5, "five" } { 4, "four" } { 1, "one" } { 6, "six" } { 3, "three" } { 2, "two" }
           by id:  { 1, "one" } { 2, "two" } { 3, "three" } { 4, "four" } { 5, "five" } { 6, "six" }
       std::less:  { 1, "one" } { 2, "two" } { 3, "three" } { 4, "four" } { 5, "five" } { 6, "six" }
    std::greater:  { 6, "six" } { 5, "five" } { 4, "four" } { 3, "three" } { 2, "two" } { 1, "one" }
           funky:  { 4, "four" } { 2, "two" } { 3, "three" } { 1, "one" } { 6, "six" } { 5, "five" }
    Removed 1
           funky:  { 4, "four" } { 2, "two" } { 3, "three" } { 6, "six" } { 5, "five" }
    Inserted the answer (appears at end)
           funky:  { 4, "four" } { 2, "two" } { 3, "three" } { 6, "six" } { 5, "five" } { 42, "answer" }
         by name:  { 42, "answer" } { 5, "five" } { 4, "four" } { 6, "six" } { 3, "three" } { 2, "two" }