Boost.Multi-index offers an extremely customisable interface, at the cost of offering an extremely complex interface, so it's easy to understand why you're stuck.
I'll present a commented example which should match your use case.
First, our data:
struct X
{
long long l; // assume unique
int i1; // assume unique
int i2; // assume non-unique
// plus any ohter data you have in your class X
};
Next, we'll prepare one tag for each index which we want the container to have. Tags aren't strictly necessary (indexes can be accessed by their order), but it's more convenient to provide a name for each index:
struct IndexByL {};
struct IndexByI1 {};
struct IndexByI2 {};
Now, we have what we need to put together the type of the container:
using Container = boost::multi_index_container<
X*, // the data type stored
boost::multi_index::indexed_by< // list of indexes
boost::multi_index::hashed_unique< //hashed index over 'l'
boost::multi_index::tag<IndexByL>, // give that index a name
boost::multi_index::member<X, long long, &X::l> // what will be the index's key
>,
boost::multi_index::ordered_unique< //ordered index over 'i1'
boost::multi_index::tag<IndexByI1>, // give that index a name
boost::multi_index::member<X, int, &X::i1> // what will be the index's key
>,
boost::multi_index::hashed_non_unique< //hashed non-unique index over 'i2'
boost::multi_index::tag<IndexByI2>, // give that index a name
boost::multi_index::member<X, int, &X::i2> // what will be the index's key
>
>
>;
That's it, we have a container. Next, here's how we can use it:
Container c; // empty container
X x1{...}, x2{...}, x3{...}; // some data
// Insert some elements
auto& indexByL = c.get<IndexByL>(); // here's where index tags (=names) come in handy
indexByL.insert(&x1);
indexByL.insert(&x2);
indexByL.insert(&x3);
// Look up by i1
auto& indexByI1 = c.get<IndexByI1>();
auto itFound = indexByI1.find(42);
if (itFound != indexByI1.end())
{
X *x = *itFound;
}
// Look up by i2
auto& indexByI2 = c.get<IndexByI2>();
size_t numberOfHundreds = indexByI2.count(100);
[Live example]
And now, some prose about how the beast works in general.
You define a multi-index container by specifying the type of objects it will store (X*
in your case), and one or more indexes which can be used to access the stored objects. Think of indexes as interfaces for accessing the data.
Indexes can be of different kinds:
- Indexes based on ordering by a key (think
std::set
or std::map
)
- Indexes based on ranking by a key (think the same plus easy access to nth element)
- Indexes based on hashing a key (think
std::unordered_set
or std::unordered_map
)
- Indexes based on accessing in stable order (think
std::list
)
- Indexes based on random access in stable order (think
std::vector
)
The key-based indexes can also be either unique (like std::map
), or non-unique (like std::multimap
).
When defining the container, you pass each index you want to have as one template argument to boost::multi_index::indexed_by
. (In our example above, I added three indexes).
For indexes which do not use a key (stable order & random access), nothing needs to be specified; you just say "I want such an index."
For key-based indexes, you also need to specify how the key is obtained from the data. That's where key extractors come into play. (These are the three uses of boost::multi_index::member
in the example). Basically, for each index, you provide a recipe (or algorithm) for deriving the key from the data stored in the container. Currently available key extractors are:
- Use the element itself:
identity
- Use a data member of the element:
member
- Use a (constant) member function of the element:
[const_]mem_fun
- Use a global function:
global_fun
- Combine multiple key extractors into one:
composite_key
Note that key extractors are able to dereference pointers transparently. That is, if your data element is a pointer to class C
, you can specify key extractors over class C
and the dereference will happen automagically. (This property is also used in the example).
This way a container with indexes is defined. To access an index, you call the get
member function template on the container. You can refer to an index by its sequential number in the list of template arguments of indexed_by
. For more readable manipulation, you can however introduce a tag for each index (or just some of them). A tag is an arbitrary type (usually an empty structure with a suitable name) which allows you to use that type as template argument for get
instead of the index's sequential number. (This is also used in the example).
Once you retrieve a reference to an index from the container, you can use it just like the data structure to which the index corresponds (map, hashset, vector, ...). Changes done through that index will affect the entire container.