Multiple indexes query in Boost Multi-Index - c++

I'm writing a software that stores GenericOrder (containing a quantity, a price, a way and a timestamp) as shared_ptr.
I've read Boost documentation and succeed to define a MultiIndexOrderContainer using three indexes: way, timestamp and price.
But I don't find a way to iterate on specific orders using multiple indexes at the same time.
#include <memory>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/identity.hpp>
#include <boost/multi_index/member.hpp>
using namespace ::boost;
using namespace ::boost::multi_index;
enum class Way
{
UNDEFINED,
BUY,
SELL
};
template <typename QuantityType, typename PriceType>
struct GenericOrder
{
explicit GenericOrder(const Way way, const QuantityType& quantity, const PriceType& price, const long long& timestamp)
: way_(way), quantity_(quantity), price_(price), timestamp_(timestamp)
{
}
~GenericOrder() = default;
GenericOrder(const GenericOrder&) = delete;
GenericOrder& operator=(const GenericOrder&) = delete;
Way way_;
QuantityType quantity_;
PriceType price_;
long long timestamp_ = -1;
};
// Aliases
using QuantityType = int;
using PriceType = int;
using OrderType = GenericOrder<QuantityType, PriceType>;
using PointerType = std::shared_ptr<OrderType>;
struct way {};
struct timestamp {};
struct price {};
using MultiIndexOrderContainer = multi_index_container<PointerType,
indexed_by<
ordered_non_unique<tag<way>, member<OrderType, decltype(OrderType::way_), &OrderType::way_ >>,
ordered_non_unique<tag<timestamp>, member<OrderType, decltype(OrderType::timestamp_), &OrderType::timestamp_ >>,
ordered_non_unique<tag<price>, member<OrderType, decltype(OrderType::price_), &OrderType::price_>>
>
>;
int main()
{
MultiIndexOrderContainer c;
// Inserting some orders
c.insert(std::make_shared<OrderType>(Way::BUY, 10, 15, 0));
c.insert(std::make_shared<OrderType>(Way::BUY, 10, 14, 1));
c.insert(std::make_shared<OrderType>(Way::BUY, 10, 13, 2));
c.insert(std::make_shared<OrderType>(Way::SELL, 10, 16, 3));
c.insert(std::make_shared<OrderType>(Way::SELL, 10, 17, 4));
c.insert(std::make_shared<OrderType>(Way::SELL, 10, 18, 5));
return 0;
}
I would like to iterate on:
On buying orders with a specific price sorted by timestamp
Cheapest order price from selling orders
Costliest order price from buying orders
How can I achieve that ?

Boost.MultiIndex does not provide any special mechanism for mixing the orders induced by different indices: with the structure you propose you're basically down to querying for the first parameter and then doing a linear scan on the returned range.
On the other hand, if your queries are always of the form (attr1, attr2, attr3) you can speed them up by using composite keys. In your particular case, you can have the three queries you're after with a composite key on (way_,price_,timestamp_)
Live On Coliru
#include <memory>
#include <iostream>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/composite_key.hpp>
#include <boost/multi_index/identity.hpp>
#include <boost/multi_index/member.hpp>
using namespace ::boost;
using namespace ::boost::multi_index;
enum class Way
{
UNDEFINED,
BUY,
SELL
};
template <typename QuantityType, typename PriceType>
struct GenericOrder
{
explicit GenericOrder(const Way way, const QuantityType& quantity, const PriceType& price, const long long& timestamp)
: way_(way), quantity_(quantity), price_(price), timestamp_(timestamp)
{
}
~GenericOrder() = default;
GenericOrder(const GenericOrder&) = delete;
GenericOrder& operator=(const GenericOrder&) = delete;
Way way_;
QuantityType quantity_;
PriceType price_;
long long timestamp_ = -1;
};
template <typename QuantityType, typename PriceType>
std::ostream& operator<<(std::ostream& os,const GenericOrder<QuantityType,PriceType>& o)
{
switch(o.way_){
case Way::UNDEFINED: os<<"UNDEFINED, ";break;
case Way::BUY: os<<"BUY, ";break;
case Way::SELL: os<<"SELL, ";break;
}
return os<<o.price_<<", "<<o.timestamp_<<"\n";
}
// Aliases
using QuantityType = int;
using PriceType = int;
using OrderType = GenericOrder<QuantityType, PriceType>;
using PointerType = std::shared_ptr<OrderType>;
struct way {};
struct timestamp {};
struct price {};
using MultiIndexOrderContainer = multi_index_container<PointerType,
indexed_by<
ordered_non_unique<
composite_key<
OrderType,
member<OrderType, decltype(OrderType::way_), &OrderType::way_ >,
member<OrderType, decltype(OrderType::price_), &OrderType::price_>,
member<OrderType, decltype(OrderType::timestamp_), &OrderType::timestamp_ >
>
>
>
>;
int main()
{
MultiIndexOrderContainer c;
// Inserting some orders
c.insert(std::make_shared<OrderType>(Way::BUY, 10, 15, 0));
c.insert(std::make_shared<OrderType>(Way::BUY, 10, 14, 1));
c.insert(std::make_shared<OrderType>(Way::BUY, 10, 13, 2));
c.insert(std::make_shared<OrderType>(Way::BUY, 10, 15, 1));
c.insert(std::make_shared<OrderType>(Way::SELL, 10, 16, 3));
c.insert(std::make_shared<OrderType>(Way::SELL, 10, 17, 4));
c.insert(std::make_shared<OrderType>(Way::SELL, 10, 18, 5));
std::cout<<"Buying orders for 15, sorted by timestamp\n";
auto p=c.equal_range(std::make_tuple(Way::BUY,15));
while(p.first!=p.second)std::cout<<**p.first++;
std::cout<<"Cheapest selling order\n";
std::cout<<**c.lower_bound(Way::SELL);
std::cout<<"Costliest buying order\n";
std::cout<<**--c.upper_bound(Way::BUY);
return 0;
}

Related

boost bimap with associated value

Is there a way to construct a boost::bimap (or multi-index container) that has two keys, plus a value they both point to? In addition, you can query one key to get the other?
I can construct a boost multi-index container that has two keys for some element, but cannot figure out how to get the value of a key given the value of the other key?
I am trying to do something like:
struct s {};
int main()
{
typedef std::map<int, std::shared_ptr<s>> left_map_type;
typedef std::map<std::string, std::shared_ptr<s>> right_map_type;
typedef boost::bimap<left_map_type, right_map_type> bi_map_type;
typedef bi_map_type::value_type value_type;
bi_map_type bim;
std::shared_ptr<s> sp = std::make_shared<s>();
std::shared_ptr<s> sp2 = std::make_shared<s>();
left_map_type lm { {1, sp}, {2, sp2} };
right_map_type rm { {"foo", sp}, {"bar", sp2} };
bim.insert(lm, rm);
/*
For instance, given the key "foo", search bim to obtain BOTH the shared pointer sp as well as the alternate key '1'.
*/
}
I would use a multi-index container over records like:
struct Data { }; // your "s"
struct Record {
int id;
std::string name;
Data data; // std::shared_ptr<Data>?
};
Now you could make a container that adds unique indexes by id and name:
using Table = boost::multi_index_container<Record,
bmi::indexed_by<
bmi::ordered_unique< bmi::tag<struct by_id>, bmi::member<Record, int, &Record::id>>,
bmi::ordered_unique< bmi::tag<struct by_name>, bmi::member<Record, std::string, &Record::name>>
> >;
Formatting more verbosely:
using Table = boost::multi_index_container<
Record,
bmi::indexed_by<
bmi::ordered_unique<
bmi::tag<struct by_id>,
bmi::member<Record, int, &Record::id>>,
bmi::ordered_unique<
bmi::tag<struct by_name>,
bmi::member<Record, std::string, &Record::name>>>>;
See below for a less verbose way;
Now you can make your table and access it using any of the indices:
Table table;
auto& left = table.get<by_id>(); // or get<0>
auto& right = table.get<by_name>(); // or get<1>
Whatever interface you use, any changes will reflect in all other indexes, and uniqueness constraints are guaranteed. E.g.
table.emplace(1, "one", Data{"Sleepy", {1, 2, 3}});
table.emplace(2, "two", Data{"Grumpy", {2, 4, 6}});
table.emplace(3, "three", Data{"Sneezy", {3, 6, 9}});
Just printing them (using libfmt for demo):
// Simple enumeration:
fmt::print("Just the table:\n - {}\n", fmt::join(table, "\n - "));
fmt::print("By id:\n - {}\n", fmt::join(left, "\n - "));
fmt::print("By name:\n - {}\n", fmt::join(right, "\n - "));
Prints
Just the table:
- Record{1, one, Data{Sleepy, {1, 2, 3}}}
- Record{2, two, Data{Grumpy, {2, 4, 6}}}
- Record{3, three, Data{Sneezy, {3, 6, 9}}}
By id:
- Record{1, one, Data{Sleepy, {1, 2, 3}}}
- Record{2, two, Data{Grumpy, {2, 4, 6}}}
- Record{3, three, Data{Sneezy, {3, 6, 9}}}
By name:
- Record{1, one, Data{Sleepy, {1, 2, 3}}}
- Record{3, three, Data{Sneezy, {3, 6, 9}}}
- Record{2, two, Data{Grumpy, {2, 4, 6}}}
This exemplifies that the default index is the first index declared (the "left view" here, or as I'd prefer to call it: by_id).
Searching is just like you'd expect from a standard container:
auto id2 = left.find(2);
auto nameTwo = right.find("two");
if (id2 != left.end())
fmt::print("id2: {}\n", *id2);
if (nameTwo != right.end())
fmt::print("nameTwo: {}\n", *nameTwo);
For non-unique indexes, equal_range is useful. There's lower_bound and upper_bound etc.
Live Demo
Live On Compiler Explorer
#include <boost/multi_index/member.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index_container.hpp>
#include <memory>
struct Data {
std::string extra;
std::vector<int> ints;
};
struct Record {
int id;
std::string name;
Data data; // std::shared_ptr<Data>?
};
namespace bmi = boost::multi_index;
#define Index(name) \
bmi::ordered_unique< \
bmi::tag<struct by_##name>, \
bmi::member<Record, decltype(Record::name), &Record::name>>
using Table = boost::multi_index_container<Record,
bmi::indexed_by<
Index(id),
Index(name)
> >;
#include <fmt/ranges.h>
template <>
struct fmt::formatter<Data, char> : fmt::formatter<std::string, char> {
auto format(Data const& data, auto& ctx) {
return fmt::format_to(ctx.out(), "Data{{{}, {}}}", data.extra,
data.ints);
}
};
template <>
struct fmt::formatter<Record, char> : fmt::formatter<std::string, char> {
auto format(Record const& rec, auto& ctx) {
return fmt::format_to(ctx.out(), "Record{{{}, {}, {}}}", rec.id,
rec.name, rec.data);
}
};
int main()
{
Table table;
auto& left = table.get<by_id>(); // or get<0>
auto& right = table.get<by_name>(); // or get<1>
table.emplace(1, "one", Data{"Sleepy", {1, 2, 3}});
table.emplace(2, "two", Data{"Grumpy", {2, 4, 6}});
table.emplace(3, "three", Data{"Sneezy", {3, 6, 9}});
// Simple enumeration:
fmt::print("Just the table:\n - {}\n", fmt::join(table, "\n - "));
fmt::print("By id:\n - {}\n", fmt::join(left, "\n - "));
fmt::print("By name:\n - {}\n", fmt::join(right, "\n - "));
// find:
auto id2 = left.find(2);
auto nameTwo = right.find("two");
if (id2 != left.end())
fmt::print("id2: {}\n", *id2);
if (nameTwo != right.end())
fmt::print("nameTwo: {}\n", *nameTwo);
}
Printing the outbove shown above.
Advanced Topics/Usages
A few things to keep in mind:
if you really needed to share ownership of the data, you can, of course use shared_ptr<Data> instead
you can also construct multi-index containers over references/pointers, so I'd advice against shared_ptr unless you know it's actually required
more flexible key extraction mechanisms exist (e.g. you could have a non-unique ordered index on Record::data::ints::length())
you can have composite keys, which support partial querying on ordered indexes. This enable "database like" queries, see e.g. Boost multi-index container vs a multi-level mapping container based on std::unordered_map (map of maps) or Using boost multi index like relational DB
like with the standard containers, the key elements are const. In multi-index containers this implies that all accessors return const references. Refer to the document for modify, replace functions.
there's a project function to convert iterators between indexes, should you ever require this
Bonus: Less Verbose?
Or, with a clever macro to reduce repetition¹
#define Index(name) \
bmi::ordered_unique< \
bmi::tag<struct by_##name>, \
bmi::member<Record, decltype(Record::name), &Record::name>>
using Table = boost::multi_index_container<Record,
bmi::indexed_by<
Index(id),
Index(name)
> >;
¹ I was momentarily too lazy to make that template meta functions instead of the macro

Conditional boost::range::join

I want to write a function like this:
template<class IterableType>
void CheckAndProcessIterables(IterableType& a, IterableType& b, IterableType& c) {
IteratorRangeType range{}; // empty range
if (Check(a)) {
range = boost::range::join(range, a);
}
if (Check(b)) {
range = boost::range::join(range, b);
}
if (Check(c)) {
range = boost::range::join(range, c);
}
Process(range);
}
Is it possible? Which type should I use instead of IteratorRangeType?
As far as I understand it, boost::range::join return type depends on it's arguments. Is there some wrapper class which can be assigned any type of range as long as its underlying value type is same?
You can use type erased iterator ranges, which Boost has in the form of any_range.
Beware of the performance cost of these, which can quickly become very noticable. I'd rethink the approach unless you're very sure that this not on any hot path and readability is a much more of a concern than performance.
Live On CoCompiler Explorer
#include <boost/range/join.hpp>
#include <boost/range/any_range.hpp>
// for demo only:
#include <boost/range/algorithm/for_each.hpp>
#include <boost/lambda/lambda.hpp>
#include <fmt/ranges.h>
template<class Range>
bool Check(Range const& r) {
bool odd_len = boost::size(r) % 2;
fmt::print("Check {}, odd_len? {}\n", r, odd_len);
return odd_len;
}
template<class Range>
void Process(Range const& r) {
fmt::print("Processing {}\n", r);
using namespace boost::lambda;
for_each(r, _1 *= _1);
}
template<class IterableType>
void CheckAndProcessIterables(IterableType& a, IterableType& b, IterableType& c) {
using V = typename boost::range_value<IterableType>::type;
using ErasedRange= boost::any_range<V, boost::forward_traversal_tag>;
ErasedRange range{}; // empty range
if (Check(a)) {
range = boost::range::join(range, a);
}
if (Check(b)) {
range = boost::range::join(range, b);
}
if (Check(c)) {
range = boost::range::join(range, c);
}
Process(range);
}
int main() {
std::vector a{1, 2, 3}, b{4, 5}, c{6, 7, 8};
CheckAndProcessIterables(a, b, c);
fmt::print("After process: a:{} b:{} c:{}\n", a, b, c);
}
Prints
Check {1, 2, 3}, odd_len? true
Check {4, 5}, odd_len? false
Check {6, 7, 8}, odd_len? true
Processing {1, 2, 3, 6, 7, 8}
After process: a:{1, 4, 9} b:{4, 5} c:{36, 49, 64}

How can I Iterate over Vertices & Edges in an Order Provided by a (Bundled) Property, in the BGL?

Say I have some boost graph
#include <boost/graph/adjacency_list.hpp>
struct Vertex {
double property_1;
int property_2;
};
using Graph_t = boost::adjacency_list<boost::listS,
boost::listS,
boost::undirectedS,
Vertex,
boost::no_property>;
Graph_t g(5);
and now want to iterate over the vertices in different orders, say:
by its id
in a random order
descending by property_2
ascending by property_1
descending/ascending by more bundled properties in a generic way.
How do I do this in the most efficient way?
As of now, I created std::vectors with the properties, and vectors containing indices, and sorted them by the properties. But if you have many properties that creates a ton of structure that could be avoided.
I also looked at boost::multi_index maps, as in this cplusplus.com question, but that doesn't seem slim to me either.
How can I do this? Happy about any hint!
Boost.MultiIndex can be plugged in in a rather convoluted, undocumented fashion:
Live Coliru Demo
#include <boost/graph/adjacency_list.hpp>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/random_access_index.hpp>
#include <boost/multi_index/ordered_index.hpp>
struct mic_tag:
/* it is assumed first index is random-access */
virtual public boost::graph_detail::random_access_container_tag,
virtual public boost::graph_detail::back_insertion_sequence_tag{};
namespace boost{
template<typename... Args>
mic_tag container_category(boost::multi_index_container<Args...>&){return {};}
}
template<typename GraphType,typename KeyExtractor>
struct vertex_adapted
{
using result_type=typename KeyExtractor::result_type;
decltype(auto) operator()(void* p)const
{
return key(
static_cast<typename GraphType::stored_vertex*>(p)->m_property);
}
KeyExtractor key;
};
struct vertex_t
{
double property_1;
int property_2;
};
struct graph_t;
struct graph_t_vertex_list;
namespace boost{
template<typename Value>
struct container_gen<graph_t_vertex_list,Value>
{
using type=boost::multi_index_container<
Value,
boost::multi_index::indexed_by<
boost::multi_index::random_access<>,
boost::multi_index::ordered_non_unique<
vertex_adapted<
graph_t,
boost::multi_index::member<vertex_t,double,&vertex_t::property_1>
>
>,
boost::multi_index::ordered_non_unique<
vertex_adapted<
graph_t,
boost::multi_index::member<vertex_t,int,&vertex_t::property_2>
>,
std::greater<int>
>
>
>;
};
}
struct graph_t:
boost::adjacency_list<
boost::listS,
graph_t_vertex_list,
boost::undirectedS,
vertex_t
>{};
/* testing */
#include <iostream>
std::ostream& operator<<(std::ostream& os,const vertex_t& v)
{
os<<"{"<<v.property_1<<","<<v.property_2<<"}";
return os;
}
int main()
{
graph_t g;
add_vertex(vertex_t{0.0,0},g);
add_vertex(vertex_t{0.1,1},g);
add_vertex(vertex_t{0.2,2},g);
for(void* p:g.m_vertices.get<1>()){
std::cout<<static_cast<graph_t::stored_vertex*>(p)->m_property;
}
std::cout<<"\n";
for(void* p:g.m_vertices.get<2>()){
std::cout<<static_cast<graph_t::stored_vertex*>(p)->m_property;
}
std::cout<<"\n";
}
Output
{0,0}{0.1,1}{0.2,2}
{0.2,2}{0.1,1}{0,0}
Update Apr 14: I've refactored things a bit so that the resulting user code is much more straightforward:
struct vertex_t
{
double property_1;
int property_2;
};
using graph_t= boost::adjacency_list<
boost::listS,
mic_listS<
boost::multi_index::ordered_non_unique<
boost::multi_index::member<vertex_t,double,&vertex_t::property_1>
>,
boost::multi_index::ordered_non_unique<
boost::multi_index::member<vertex_t,int,&vertex_t::property_2>,
std::greater<int>
>
>,
boost::undirectedS,
vertex_t
>;
Complete code follows:
Live Coliru Demo
#include <boost/graph/adjacency_list.hpp>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/random_access_index.hpp>
template<typename KeyExtractor>
struct mic_list_key_extractor
{
using result_type=typename KeyExtractor::result_type;
template<typename StoredVertex>
decltype(auto) operator()(StoredVertex& v)const{return key(v.m_property);}
KeyExtractor key;
};
template<typename IndexSpecifier,typename=void>
struct mic_list_index_specifier{using type=IndexSpecifier;};
template<
template<typename...> class IndexSpecifier,
typename Arg1,typename Arg2,typename... Args
>
struct mic_list_index_specifier<
IndexSpecifier<Arg1,Arg2,Args...>,
std::void_t<typename IndexSpecifier<Arg1,Arg2,Args...>::key_from_value_type>>
{
static constexpr bool has_tag=boost::multi_index::detail::is_tag<Arg1>::value;
using arg1=std::conditional_t<has_tag,Arg1,mic_list_key_extractor<Arg1>>;
using arg2=std::conditional_t<has_tag,mic_list_key_extractor<Arg2>,Arg2>;
using type=IndexSpecifier<arg1,arg2,Args...>;
};
template<typename IndexSpecifier>
using mic_list_index_specifier_t=
typename mic_list_index_specifier<IndexSpecifier>::type;
template<typename Value,typename... IndexSpecifiers>
struct mic_list:boost::multi_index_container<
Value,
boost::multi_index::indexed_by<
boost::multi_index::random_access<>,
mic_list_index_specifier_t<IndexSpecifiers>...
>
>
{};
template<typename... IndexSpecifiers>
struct mic_listS;
struct mic_list_tag:
virtual public boost::graph_detail::random_access_container_tag,
virtual public boost::graph_detail::back_insertion_sequence_tag{};
namespace boost{
template<typename... Args>
mic_list_tag container_category(const mic_list<Args...>&){return {};}
template<typename Value,typename... IndexSpecifiers>
struct container_gen<mic_listS<IndexSpecifiers...>,Value>
{
using type=mic_list<Value,IndexSpecifiers...>;
};
namespace detail
{
template<typename... IndexSpecifiers>
struct is_random_access<mic_listS<IndexSpecifiers...>>
{
static constexpr bool value=true;
using type=boost::mpl::true_;
};
}
}
/* testing */
#include <boost/multi_index/ordered_index.hpp>
#include <iostream>
struct vertex_t
{
double property_1;
int property_2;
};
using graph_t= boost::adjacency_list<
boost::listS,
mic_listS<
boost::multi_index::ordered_non_unique<
boost::multi_index::member<vertex_t,double,&vertex_t::property_1>
>,
boost::multi_index::ordered_non_unique<
boost::multi_index::member<vertex_t,int,&vertex_t::property_2>,
std::greater<int>
>
>,
boost::undirectedS,
vertex_t
>;
std::ostream& operator<<(std::ostream& os,const vertex_t& v)
{
os<<"{"<<v.property_1<<","<<v.property_2<<"}";
return os;
}
int main()
{
graph_t g;
add_vertex(vertex_t{0.0,0},g);
add_vertex(vertex_t{0.1,1},g);
add_vertex(vertex_t{0.2,2},g);
for(const auto& v:g.m_vertices.get<1>()){
std::cout<<v.m_property;
}
std::cout<<"\n";
for(const auto& v:g.m_vertices.get<2>()){
std::cout<<v.m_property;
}
std::cout<<"\n";
}
Output
{0,0}{0.1,1}{0.2,2}
{0.2,2}{0.1,1}{0,0}
That's (obviously) not a feature of the library.
You can however use ranges or range adaptors, like you would in any other situation:
Live On Coliru
#include <boost/graph/adjacency_list.hpp>
#include <boost/range/adaptors.hpp>
#include <boost/range/algorithm.hpp>
#include <boost/range/algorithm_ext.hpp>
#include <fmt/ranges.h>
#include <fmt/ostream.h>
#include <random>
struct Vertex {
double property_1;
int property_2;
};
static inline std::ostream& operator<<(std::ostream& os, Vertex const& v) {
return os << "V(" << v.property_1 << ", " << v.property_2 << ")";
}
using Graph_t =
boost::adjacency_list<boost::listS, boost::listS, boost::undirectedS,
Vertex, boost::no_property>;
int main() {
using boost::make_iterator_range;
using namespace boost::adaptors;
Graph_t g(5);
int i = 0;
for (auto& v : make_iterator_range(vertices(g))) {
++i;
g[v] = {i / -.3, i * 11};
}
auto get_bundle = [&g](auto v) -> auto& { return g[v]; };
fmt::print("Natural order: {}\n",
make_iterator_range(vertices(g)));
fmt::print("Natural order: {}\n",
make_iterator_range(vertices(g) | transformed(get_bundle)));
fmt::print(
"Reverse natural order: {}\n",
make_iterator_range(vertices(g) | transformed(get_bundle) | reversed));
auto make_view = [=](auto range) {
return std::vector<std::reference_wrapper<Vertex>>(
begin(range), end(range));
};
auto view =
make_view(make_iterator_range(vertices(g) | transformed(get_bundle)));
boost::reverse(view);
fmt::print("view: {}\n", view);
boost::reverse(view);
fmt::print("reversed: {}\n", view);
auto less_by = [](auto member) {
return [=, prj = std::mem_fn(member)](auto const& a, auto const& b) {
return prj(a) < prj(b);
};
};
boost::sort(view, less_by(&Vertex::property_1));
fmt::print("less_by property_1: {}\n", view);
boost::sort(view, less_by(&Vertex::property_2));
fmt::print("less_by property_2: {}\n", view);
{
static std::random_device rd;
static std::mt19937 randgen{rd()};
std::shuffle(view.begin(), view.end(), randgen);
fmt::print("random order: {}\n", view);
}
// just a loop is also fine, of course
i = 0;
for (Vertex& bundle : view) {
bundle.property_2 = i++;
}
fmt::print("modified: {}\n", view);
}
Prints
Natural order: {0x1467eb0, 0x1467f10, 0x1467f70, 0x1467fd0, 0x1468030}
Natural order: {V(-3.33333, 11), V(-6.66667, 22), V(-10, 33), V(-13.3333, 44), V(-16.6667, 55)}
Reverse natural order: {V(-16.6667, 55), V(-13.3333, 44), V(-10, 33), V(-6.66667, 22), V(-3.33333, 11)}
view: {V(-16.6667, 55), V(-13.3333, 44), V(-10, 33), V(-6.66667, 22), V(-3.33333, 11)}
reversed: {V(-3.33333, 11), V(-6.66667, 22), V(-10, 33), V(-13.3333, 44), V(-16.6667, 55)}
less_by property_1: {V(-16.6667, 55), V(-13.3333, 44), V(-10, 33), V(-6.66667, 22), V(-3.33333, 11)}
less_by property_2: {V(-3.33333, 11), V(-6.66667, 22), V(-10, 33), V(-13.3333, 44), V(-16.6667, 55)}
random order: {V(-13.3333, 44), V(-3.33333, 11), V(-10, 33), V(-6.66667, 22), V(-16.6667, 55)}
modified: {V(-13.3333, 0), V(-3.33333, 1), V(-10, 2), V(-6.66667, 3), V(-16.6667, 4)}
More, From Here
std::ranges could give you most of these but in my experience has a few more limitations. However, it will be generally safer (because Boost Range V2 is quite old).
to have "living indexes" (like a database) make your vertex container selector select a Multi Index Container. See e.g. the advice here https://marc.info/?l=boost&m=118835654637830
to model your own graph datastructure, see e.g. here for inspiration
What is needed to use BGL algorithms on existing data structures ( edges and vertices as vector<Object *>)?
What is required for a custom BGL graph to work with topological sort?
UPDATE Code Generation With Boost PFR
In response to the comments, you could use Boost PFR to generate a array with comparators simple types statically:
template <typename T, typename Op = std::less<> >
constexpr static inline auto make_field_comparers(Op op = {}) {
namespace pfr = boost::pfr;
auto constexpr N = pfr::tuple_size<T>::value;
using A = std::array<std::function<bool(T const&, T const&)>, N>;
auto lift = [op](auto prj) {
return [=](T const& a, T const& b) { return op(prj(a), prj(b)); };
};
return [lift]<size_t... I>(std::index_sequence<I...>){
return A{lift([](T const& v) { return pfr::get<I>(v); })...};
}
(std::make_index_sequence<N>{});
}
Which you could use like Live On Compiler Explorer
std::vector orderings {
std::pair { "asc", make_field_comparers<Vertex>() },
std::pair { "desc", make_field_comparers<Vertex>(std::greater<>{}) },
};
for (auto const& [dir, fields] : orderings) {
for (size_t field = 0; field < fields.size(); ++field) {
boost::sort(view, fields[field]);
fmt::print("by field #{} {}: {}\n", field, dir, view);
}
}
Printing
by field #0 asc: {V(-16.6667, 55), V(-13.3333, 44), V(-10, 33), V(-6.66667, 22), V(-3.33333, 11)}
by field #1 asc: {V(-3.33333, 11), V(-6.66667, 22), V(-10, 33), V(-13.3333, 44), V(-16.6667, 55)}
by field #0 desc: {V(-3.33333, 11), V(-6.66667, 22), V(-10, 33), V(-13.3333, 44), V(-16.6667, 55)}
by field #1 desc: {V(-16.6667, 55), V(-13.3333, 44), V(-10, 33), V(-6.66667, 22), V(-3.33333, 11)}

C++ Single Layer Multi Output Perceptron Weird Behaviour

Some background:
I wrote a single layer multi output perceptron class in C++. It uses the typical WX + b discriminant function and allows for user-defined activation functions. I have tested everything pretty throughly and it all seems to be working as I expect it to. I noticed a small logical error in my code, and when I attempted to fix it the network performed much worse than before. The error is as follows:
I evaluate the value at each output neuron using the following code:
output[i] =
activate_(std::inner_product(weights_[i].begin(), weights_[i].end(),
features.begin(), -1 * biases_[i]));
Here I treat the bias input as a fixed -1, but when I apply the learning rule to each bias, I treat the input as +1.
// Bias can be treated as a weight with a constant feature value of 1.
biases_[i] = weight_update(1, error, learning_rate_, biases_[i]);
So I attempted to fix my mistake by changing the call to weight_updated to be conistent with the output evaluation:
biases_[i] = weight_update(-1, error, learning_rate_, biases_[i]);
But doing so results in a 20% drop in accuracy!
I have been pulling my hair out for the past few days trying to find some other logical error in my code which might explain this strange behaviour, but have come up empty handed. Can anyone with more knowledge than I provide any insight into this? I have provided the entire class below for reference. Thank you in advance.
#ifndef SINGLE_LAYER_PERCEPTRON_H
#define SINGLE_LAYER_PERCEPTRON_H
#include <cassert>
#include <functional>
#include <numeric>
#include <vector>
#include "functional.h"
#include "random.h"
namespace qp {
namespace rf {
namespace {
template <typename Feature>
double weight_update(const Feature& feature, const double error,
const double learning_rate, const double current_weight) {
return current_weight + (learning_rate * error * feature);
}
template <typename T>
using Matrix = std::vector<std::vector<T>>;
} // namespace
template <typename Feature, typename Label, typename ActivationFn>
class SingleLayerPerceptron {
public:
// For testing only.
SingleLayerPerceptron(const Matrix<double>& weights,
const std::vector<double>& biases, double learning_rate)
: weights_(weights),
biases_(biases),
n_inputs_(weights.front().size()),
n_outputs_(biases.size()),
learning_rate_(learning_rate) {}
// Initialize the layer with random weights and biases in [-1, 1].
SingleLayerPerceptron(std::size_t n_inputs, std::size_t n_outputs,
double learning_rate)
: n_inputs_(n_inputs),
n_outputs_(n_outputs),
learning_rate_(learning_rate) {
weights_.resize(n_outputs_);
std::for_each(
weights_.begin(), weights_.end(), [this](std::vector<double>& wv) {
generate_back_n(wv, n_inputs_,
std::bind(random_real_range<double>, -1, 1));
});
generate_back_n(biases_, n_outputs_,
std::bind(random_real_range<double>, -1, 1));
}
std::vector<double> predict(const std::vector<Feature>& features) const {
std::vector<double> output(n_outputs_);
for (auto i = 0ul; i < n_outputs_; ++i) {
output[i] =
activate_(std::inner_product(weights_[i].begin(), weights_[i].end(),
features.begin(), -1 * biases_[i]));
}
return output;
}
void learn(const std::vector<Feature>& features,
const std::vector<double>& true_output) {
const auto actual_output = predict(features);
for (auto i = 0ul; i < n_outputs_; ++i) {
const auto error = true_output[i] - actual_output[i];
for (auto weight = 0ul; weight < n_inputs_; ++weight) {
weights_[i][weight] = weight_update(
features[weight], error, learning_rate_, weights_[i][weight]);
}
// Bias can be treated as a weight with a constant feature value of 1.
biases_[i] = weight_update(1, error, learning_rate_, biases_[i]);
}
}
private:
Matrix<double> weights_; // n_outputs x n_inputs
std::vector<double> biases_; // 1 x n_outputs
std::size_t n_inputs_;
std::size_t n_outputs_;
ActivationFn activate_;
double learning_rate_;
};
struct StepActivation {
double operator()(const double x) const { return x > 0 ? 1 : -1; }
};
} // namespace rf
} // namespace qp
#endif /* SINGLE_LAYER_PERCEPTRON_H */
I ended up figuring it out...
My fix was indeed correct and the loss of accuracy was just a consequence of having a lucky (or unlucky) dataset.

Boost Multi Index: index based on list content

I'm new to Boost Multi Index container, and was wondering if it could solve in a more effecient way my problem, simplified thus:
struct A {
int id;
}
struct B {
int id;
std::list<A> products;
}
Each A having a strictly unique ID, I want to be able with a single multi index container, be able to look for B.
By B's id, and by A's Id.
At the moment i'm working with nice std::map, and map linking A ids to B ids.
So to say. It works fine enough. But, I'm having other parameters for such a look up and it's getting really nasty :)
EDIT:
As per comment request I'll elaborate somewhat:
I've Satellites, which in turn have many Transponders and many Sources.
I want to be able to find a Satellite for a given Transponder id or Source id( which are indeed unique )
Sadly I don't have hand on Satellite struct, means, I can't alter it.
Briefly it looks like that :
struct Satellite {
int norad_id;
std::list<Transponder> transponders;
std::list<Source> sources;
... some more data
}
What I want to do is simply search a whatever of Satellites, and find a Satellite having a specific transponder-, or source-, or norad id.
At the moment, I'm using 3 nice maps
std::map<int /*norad*/ ,Satellite> satellites;
std::map<int /*transponder id*/, int/* norad */> transponder_to_satellite;
std::map<int /* source_id */, int /* norad */ > source_to_satellite;
From the example #sehe provided, I see it would be somewhat easier if I were to spawn a relationnal struct. I guess I'll give it a try ... :)
In the absense of exact use cases, here' some suggestions to model the indices based on what you showed¹
struct Product {
int id;
};
struct Category {
int id;
};
struct ProductCategoryRelation {
int productId;
int categoryId;
};
namespace bmi = boost::multi_index;
using RelationTable = bmi::multi_index_container<
ProductCategoryRelation,
bmi::indexed_by<
bmi::ordered_unique<
bmi::tag<struct by_product>,
bmi::member<ProductCategoryRelation, int, &ProductCategoryRelation::productId>
>,
bmi::ordered_unique<
bmi::tag<struct by_category>,
bmi::member<ProductCategoryRelation, int, &ProductCategoryRelation::categoryId>
>
>
>;
You could also get quite smart with a composite key, which is versatile in ordered_* indexes:
using RelationTable = bmi::multi_index_container<
ProductCategoryRelation,
bmi::indexed_by<
bmi::ordered_unique<
bmi::tag<struct by_product>,
bmi::composite_key<ProductCategoryRelation,
bmi::member<ProductCategoryRelation, int, &ProductCategoryRelation::categoryId>,
bmi::member<ProductCategoryRelation, int, &ProductCategoryRelation::productId>
>
>
>
>;
Here's a small demo:
Live On Coliru
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/member.hpp>
#include <boost/multi_index/mem_fun.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/hashed_index.hpp>
#include <boost/multi_index/composite_key.hpp>
#include <boost/multi_index/global_fun.hpp>
#include <list>
struct Product {
int id;
};
struct Category {
int id;
};
struct ProductCategoryRelation {
int productId;
int categoryId;
};
namespace bmi = boost::multi_index;
using RelationTable = bmi::multi_index_container<
ProductCategoryRelation,
bmi::indexed_by<
bmi::ordered_unique<
bmi::tag<struct compound>,
bmi::composite_key<ProductCategoryRelation,
bmi::member<ProductCategoryRelation, int, &ProductCategoryRelation::categoryId>,
bmi::member<ProductCategoryRelation, int, &ProductCategoryRelation::productId>
>
>
>
>;
#include <iostream>
#include <boost/range/iterator_range.hpp>
int main() {
RelationTable table {
ProductCategoryRelation { 1, 7 },
ProductCategoryRelation { 2, 7 },
ProductCategoryRelation { 3, 7 },
ProductCategoryRelation { 4, 6 },
ProductCategoryRelation { 5, 6 },
ProductCategoryRelation { 6, 6 },
ProductCategoryRelation { 7, 5 },
ProductCategoryRelation { 8, 5 },
ProductCategoryRelation { 9, 5 },
};
// find all products in category 6:
for (auto& rel : boost::make_iterator_range(table.get<compound>().equal_range(6)))
std::cout << "Product " << rel.productId << " is in category " << rel.categoryId << "\n";
}
Prints:
Product 4 is in category 6
Product 5 is in category 6
Product 6 is in category 6
¹ I crystal-balled the class names into something "realistic"