1

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.

Emil Ahlbäck
  • 6,085
  • 8
  • 39
  • 54

1 Answers1

1

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

  • add an element and it is displayed as the last item regardless of the ordering of the rest of the set
  • select a custom sort order that doesn't exactly match one of the BMI indices

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" }

sehe
  • 374,641
  • 47
  • 450
  • 633
  • 1
    (it just crossed my mind that a `stable_sort` might be more topical here, but that's a detail) – sehe Jun 28 '20 at 13:36
  • 1
    Oh, and I guess the elephant in the room: the indexes have differing static types, and therefore differing interfaces, which is why you wouldn't be able to store a pointer to them in one type [unless you go off the deep end with variants or type erasure] – sehe Jun 28 '20 at 13:38
  • Thank you for the answer, this is already very helpful. The elephant in the room that you describe in your second comment is what I was hoping to achieve. I can't figure out the signature of the variant though, perhaps if you do you could add that to the answer? – Emil Ahlbäck Jun 28 '20 at 13:43
  • Adding that to the answer would deeply confuse it. I could see it as a separate answer, but really, there was a reason why I called it "going off the deep end". I do not think it's a sane approach. You won't get nearly the full functionality that this gives and it will take extreme effort - likely leading to error-prone code. – sehe Jun 28 '20 at 13:46
  • 1
    Makes sense. I'll try to adapt my current code to reflect what you wrote and see how it goes. Again, thank you for the answer :) – Emil Ahlbäck Jun 28 '20 at 13:47
  • I see now why this is better. Also helps that this now supports "dynamic" indices with the run-time provided comparison function. Kudos again :) – Emil Ahlbäck Jun 28 '20 at 13:51
  • One more follow-up (sorry :)): There is a limitation now that only one view could use the same collection. If I had a Collection and I'd want to build, say, a view that shows all of them, and a view that shows the latest -- I couldn't achieve that here since I can get a reference to the View-tagged index, but there's only one of them. What would you suggest to overcome it? – Emil Ahlbäck Jun 28 '20 at 14:16
  • That's what I was hinting at with the "aside". I guess all you really need is the `vector>`. – sehe Jun 28 '20 at 15:03
  • If you want the enhanced features (e.g. under mutation) then I'd suggest that you can add multiple view interfaces: **[Live On Coliru](http://coliru.stacked-crooked.com/a/ba529785795148c1)** (va,vb,vc,vd,ve are five independent views). – sehe Jun 28 '20 at 15:04
  • Here's a **[direct refactor](http://coliru.stacked-crooked.com/a/131a8d9dfb140433)** of [that](http://coliru.stacked-crooked.com/a/ba529785795148c1) to use type erasure. I cheated a little by using `any_range<>` when really I needed `any_iterator<>` (but that's not public library interface). Now the usual "don't iterate a changing collection" is somewhat exacerbated - but everything runs clear under UBsan/Asan. – sehe Jun 28 '20 at 15:54
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/216830/discussion-between-sehe-and-emil-ahlback). – sehe Jun 28 '20 at 15:54