1

I have an application where, first, an std::vector<int> object is generated. Then, some operations need to be performed on this object viewed as an std::set<int> where the order does not matter and repetitions don't count.

At present, I explicitly construct an object of type std::set<int> from the std::vector<int> object. An example is presented below:

#include <cstdio>
#include <set>
#include <vector>

void printset(std::set<int>& Set) {
    printf("Printing Set Elements: ");
    for (std::set<int>::iterator siter = Set.begin(); siter != Set.end(); ++siter) {
        int val = *siter;
        printf("%d ", val);
    }
    printf("\n");
}

void printvec(std::vector<int>& Vec) {
    printf("Printing Vec Elements: ");
    for (size_t i = 0, szi = Vec.size(); i < szi; ++i) {
        int val = Vec[i];
        printf("%d ", val);
    }
    printf("\n");
}

int main()
{
    std::vector<int> VecInt{ 6, 6, 5, 5, 4, 4 };
    std::set<int> SetInt(VecInt.begin(), VecInt.end());
    printvec(VecInt);
    printset(SetInt);
}

I am trying to see if I can use Boost.MultiIndex for this purpose. One introduction to Boost.MultiIndex states:

Boost.MultiIndex can be used if elements need to be accessed in different ways and would normally need to be stored in multiple containers. Instead of having to store elements in both a vector and a set and then synchronizing the containers continuously, you can define a container with Boost.MultiIndex that provides a vector interface and a set interface.

and this is precisely what I am doing (using multiple containers and then creating one from the other constantly) while I would like to create a (multi-index) container once and then provide a vector interface and a set interface.

On looking through various examples, for e.g., here and here, it is not clear how those examples can be modified to the code example above.

Ideally, I would like to do the following in the code example above:

MultiIndexContainer vec_set_container;

vec_set_container.push_back(6);//or anything equivalent for the MultiIndexContainer
vec_set_container.push_back(6);
vec_set_container.push_back(5);
vec_set_container.push_back(5);
vec_set_container.push_back(4);
vec_set_container.push_back(4);

printvec(vec_set_container.Some_Function_That_Exposes_Vector_Interface());
printset(vec_set_container.Some_Function_That_Exposes_Set_Interface());

How can this be accomplished using Boost.MultiIndex ?

Tryer
  • 3,580
  • 1
  • 26
  • 49
  • Have you taken a look at the [documentation](https://www.boost.org/doc/libs/release/libs/multi_index/doc/index.html)? – eerorika Jan 23 '22 at 12:40
  • @eerorika Yes. The actual documentation in its simplest example directly goes on to construct a complicated struct that contains an employee name and employee id. This is also the case of both the examples I have linked to in the OP. In the case I am looking at, my objects stored in the container are all plain integers. Hence the OP. – Tryer Jan 23 '22 at 12:49

1 Answers1

1

Random access index would match the "vector" interface.

An ordered unique index would match the "set" interface.

However, if you have a unique index, this will prevent insertion of duplicates. So, you would get:

Live On Compiler Explorer

#include <boost/multi_index/identity.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/random_access_index.hpp>
#include <boost/multi_index_container.hpp>
#include <fmt/ranges.h>

namespace bmi = boost::multi_index;

using Table = bmi::multi_index_container< //
    int,
    bmi::indexed_by<
        bmi::random_access<bmi::tag<struct asVector>>,
        bmi::ordered_unique<bmi::tag<struct asSet>, bmi::identity<int>>>>;

int main()
{
    Table data{ 6, 6, 5, 5, 4, 4 };

    fmt::print("As vec {}\nAs set {}\n", //
               data.get<asVector>(),    //
               data.get<asSet>());
}

Printing

As vec {6, 5, 4}
As set {4, 5, 6}

Now, I think the "best" you could do with this is to make the order index non-unique (so, mimicking a std::multiset instead of std::set): Live On Compiler Explorer

bmi::ordered_non_unique<bmi::tag<struct asSet>, bmi::identity<int>>

Printing

As vec [6, 6, 5, 5, 4, 4]
As set {4, 4, 5, 5, 6, 6}

If you want to traverse unique elements, using a range adaptor would be minimally costly:

  1. Using Boost Live

    fmt::print("As vec {}\nAs set {}\n", //
               data.get<asVector>(),     //
               data.get<asSet>() | boost::adaptors::uniqued);
    
  2. Using RangeV3 Live

    fmt::print("As vec {}\nAs set {}\n", //
               data.get<asVector>(),     //
               data.get<asSet>() | ranges::views::unique);
    
  3. Using Standard Ranges; I couldn't make this work but it should really be something like std::ranges::unique(data.get<asSet>())

All printing

As vec {6, 6, 5, 5, 4, 4}
As set {4, 5, 6}

Other Ideas

If you don't require random access, then sequenced index might be preferrable for you. And note that this interface comes with the handy unique() and sort() methods (just like std::list).

UPDATE To The Comments

Here's a rolled-in-one response to the comments:

Live On Compiler Explorer

#include <boost/multi_index/identity.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/sequenced_index.hpp>
#include <boost/multi_index_container.hpp>
#include <fmt/ranges.h>
#include <random>

namespace bmi = boost::multi_index;

template <typename T>
using ListSet = bmi::multi_index_container< //
    T,
    bmi::indexed_by<
        bmi::sequenced<bmi::tag<struct byInsertion>>,                   //
        bmi::ordered_unique<bmi::tag<struct Ordered>, bmi::identity<T>> //
        >>;

int main()
{
    ListSet<int> data;

    std::mt19937 prng{99}; // "random" seed
    for (std::uniform_int_distribution d(1, 10); data.size() < 5;) {
        int el = d(prng);
        if (auto [it, unique] = data.push_back(el); not unique) {
            fmt::print("Element {} duplicates index #{}\n", el,
                    std::distance(begin(data), it));
        }
    }

    fmt::print("By insertion {}\nOrdered {}\n", data, data.get<Ordered>());
}

Prints

Element 9 duplicates index #3
Element 8 duplicates index #1
By insertion [7, 8, 5, 9, 1]
Ordered {1, 5, 7, 8, 9}
sehe
  • 374,641
  • 47
  • 450
  • 633
  • 1
    Thanks @sehe for a detailed answer, as always. This gives me enough to chew on. It also solves my problem in case where the vector is guaranteed to not contain repetitions. The behavior you have specified in your answer is exactly going to solve my problem. – Tryer Jan 23 '22 at 13:17
  • if Instead of initializing the multiindex container at declaration itself, what would change if I were to insert elements into it sequentially, as in a for loop, say? Would I have to say, `data.get.push_back(83);` and `data.get.insert(29);` and so on? – Tryer Jan 23 '22 at 13:21
  • Out on a limb, I'm going to suggest that you wanted a unique list that retains insertion order: https://compiler-explorer.com/z/Mcn6eGPb6 - Naming is important! – sehe Jan 23 '22 at 13:25
  • 1
    On the topic of insertion, really the docs should help you. But here goes: https://compiler-explorer.com/z/cffjY8sff – sehe Jan 23 '22 at 13:32
  • Hmm...Is it at all possible to have other functions that accept explicitly a reference to `std::vector` or `std::set` be passed *slices* of the multiindexed container that have only the vector or only the set? I tried that https://compiler-explorer.com/z/4fjjYn73E but that does not compile. – Tryer Jan 23 '22 at 13:48
  • Of course. Just adapt your mental view to reality, instead of vice versa: https://compiler-explorer.com/z/ozYq6qcfW The "slices" of the container are _indexes_, not standard containers - because if you wanted that you wouldn't be using `multi_index_container`... – sehe Jan 23 '22 at 14:22
  • (I would be remiss if I didn't point out that a modern approach would be https://compiler-explorer.com/z/aMKon6MW1) – sehe Jan 23 '22 at 16:38
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/241333/discussion-between-tryer-and-sehe). – Tryer Jan 23 '22 at 16:49