提升具有关联值的双图

boost bimap with associated value

有没有办法构建一个 boost::bimap(或多索引容器),它有两个键,加上它们都指向的一个值?另外,可以查询一个key得到另一个吗?

我可以构造一个 boost 多索引容器,它对某个元素有两个键,但无法弄清楚如何在给定另一个键的值的情况下获取一个键的值?

我正在尝试做类似的事情:

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'.  
*/   
}  

我会在像这样的记录上使用多索引容器:

struct Data { }; // your "s"

struct Record {
    int         id;
    std::string name;
    Data        data; // std::shared_ptr<Data>?
};

现在您可以制作一个容器,通过 ID 和名称添加唯一索引:

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>>
      > >;

格式化更详细:

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;

现在您可以创建 table 并使用任何索引访问它:

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}});

只是打印它们(使用 libfmt 进行演示):

// 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 - "));

版画

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

这说明默认索引是第一个声明的索引(这里的“左视图”,或者我更喜欢这样称呼它:by_id)。

搜索就像您对标准容器的期望一样:

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);

对于非唯一索引,equal_range 很有用。有 lower_boundupper_bound 等等

现场演示

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);
}

正在打印上图。

高级Topics/Usages

需要记住的几点:

  • 如果您确实需要共享数据的所有权,当然可以使用 shared_ptr<Data>
  • 您还可以在 references/pointers 上构建多索引容器,因此我建议您不要 shared_ptr 除非您知道它实际上是必需的
  • 存在更灵活的密钥提取机制(例如,您可以在 Record::data::ints::length() 上有一个非唯一的有序索引)
  • 你可以有组合键,它支持对有序索引的部分查询。这会启用“类似数据库”的查询,请参见例如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
  • 与标准容器一样,关键元素是 const。在多索引容器中,这意味着所有访问器 return const 引用。 modifyreplace函数请参考文档。
  • 有一个 project 函数可以在索引之间转换迭代器,如果您需要这个

奖励:更简洁?

或者,使用巧妙的宏来减少重复¹

#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)
      > >;

¹ 我暂时懒得制作该模板元函数而不是宏