第10.7章: 映射:带值的键
1. 读取映射元素
映射元素有两半:永不改变的键,以及属于你的、可读可写的映射值。两者都通过具名成员访问:
#include <fast_io_dsal/str_btree_map.h>
#include <fast_io.h>
using namespace ::fast_io::io;
int main()
{
::fast_io::str_btree_map<::std::size_t> m{{"c", 3zu}, {"a", 1zu}, {"b", 2zu}};
auto it{m.find("b")};
if (it != m.cend())
{
println(it->key(), " = ", it->mapped());
}
for (auto e : m)
{
println(" ", e.key(), " = ", e.mapped());
}
}
b = 2
a = 1
b = 2
c = 3
key() 返回指向容器自己那份键副本的 ::fast_io::cstring_view。mapped() 返回对值的引用,因此它也是你写入值的方式:
for (auto e : m)
{
e.mapped() *= 10zu;
}
println(m.find("c")->mapped()); // 30
for (auto e : m),而 Swiss 的对应写法是 for (auto &e : sm)(第10.9章)。两者都允许你通过 e.mapped() 写入;代理对象只是一个 (node, position) 对,所以按值复制。
2. 写入一个值
读永远不会改变映射,所以就是 find 加 mapped()。写则用一个赋值函数明确表达意图:
::fast_io::str_btree_map<::std::size_t> m{{"a", 1zu}};
m.insert_key_or_assign("a", 100zu); // replace the value of an existing key
println(m.find("a")->mapped()); // 100
m.insert_key_or_assign("b", 2zu); // insert, because "b" is new
println(m.find("b")->mapped()); // 2
100
2
调用处写着“插入或赋值”,所以不存在“键不存在时会不会被创建”的疑问:会创建,而且你传的值就是它得到的值。当你需要知道发生的是哪一种时,返回值告诉你 — true 表示映射变大了,false 表示已有的值被替换了:
println(m.insert_key_or_assign("b", 3zu)); // 0 — replaced
println(m.insert_key_or_assign("c", 4zu)); // 1 — inserted
如果你只想在键不存在时插入、而保留已有值不动,那就是 insert_key,它只做一件事:
m.insert_key("c", 999zu); // ignored: "c" is already there
println(m.find("c")->mapped()); // 4
find/find_key,每次写都是 insert_key / insert_key_or_assign / emplace_key。一次调用只做一件事,所以键写错了也绝不会悄悄把你的映射变大,也不会构造出你没要求的东西。
3. 插入函数家族
三个函数覆盖了“键已存在时该怎么办”的所有组合:
| 调用 | 键已存在 | 键是新的 |
|---|---|---|
insert_key(k, v) |
保留旧值,什么都不做。 | 插入 v。 |
insert_key_or_assign(k, v) |
用 v 覆盖。 |
插入 v。 |
emplace_key(k, args...) |
保留旧值,什么都不做。 | 用 args 就地构造值。 |
当值的构造成本很高时,emplace_key 才有意义:只有当键确实是新的时才构造值;而 insert_key(k, make_value()) 会先构造临时量,遇到重复再把它丢掉。
::fast_io::str_swiss_map<::std::size_t> sm;
sm.insert_key_or_assign("a", 1zu); // inserts, because "a" is new
sm.insert_key_or_assign("a", 100zu); // replaces the old value
println(sm.find_key("a")->mapped()); // 100
4. 就地修改值
当值不是被整体替换、而是就地更新时(+=、push_back、计数器),就用遍历。B 树映射给出的是小代理对象,所以循环按值绑定,仍然通过 mapped() 写入:
for (auto e : m)
{
e.mapped() *= 10zu;
}
println(m.find("c")->mapped()); // 40
Swiss 映射给出的是引用,所以循环按引用绑定:
::fast_io::str_swiss_map<::std::size_t> sm;
sm.insert_key_or_assign("a", 1zu);
for (auto &e : sm)
{
e.mapped() += 41zu;
}
println(sm.find_key("a")->mapped()); // 42
在遍历之外,Swiss 映射的值通过赋一个新值来写:sm.insert_key_or_assign("a", 100zu)。读它则是 sm.find_key("a")->mapped()。
5. 计数
统计出现次数是经典的映射循环:先把键查出来,然后要么插入第一次的计数,要么把已经找到的值加一。
#include <fast_io_dsal/str_btree_map.h>
#include <fast_io.h>
using namespace ::fast_io::io;
int main()
{
::fast_io::str_btree_map<::std::size_t> counts;
for (auto w : {::fast_io::cstring_view("red"), ::fast_io::cstring_view("blue"),
::fast_io::cstring_view("red"), ::fast_io::cstring_view("green"),
::fast_io::cstring_view("red")})
{
auto it{counts.find(w)};
if (it == counts.cend())
{
counts.emplace_key(w, 1zu);
}
else
{
++it->mapped();
}
}
for (auto const &e : counts)
{
println(e.key(), " appears ", e.mapped(), " time(s)");
}
}
blue appears 1 time(s)
green appears 1 time(s)
red appears 3 time(s)
由于 str_btree_map 按有序顺序遍历,报告不用第二遍就自然按键排序。B 树映射有两个性质让这个循环这么短:find 给出的迭代器的 mapped() 可写,所以第二次出现只需 ++it->mapped()。
在 Swiss 映射上 find_key 返回的是 const_iterator,所以同样的循环要么靠遍历找到要加一的元素(第10.9章),要么通过 insert_key_or_assign 替换值:
::fast_io::str_swiss_map<::std::size_t> scounts;
for (auto w : {::fast_io::cstring_view("red"), ::fast_io::cstring_view("blue"),
::fast_io::cstring_view("red")})
{
if (scounts.emplace_key(w, 1zu).inserted)
{
continue;
}
for (auto &e : scounts)
{
if (e.key() == w)
{
++e.mapped();
break;
}
}
}
要点
- 映射元素暴露
key()(只读视图)和mapped()(引用)— 从不是first/second。 - 通过
e.mapped()写入;B 树映射按代理对象的值遍历,Swiss 映射按引用遍历。 - 读用
find/find_key;写用insert_key、insert_key_or_assign或emplace_*— 一次调用只做一件事。 insert_key保留旧值,insert_key_or_assign覆盖,emplace_*就地构造。insert_key_or_assign只在真正插入时返回true,所以它同时回答了“这个键原本是新的吗?”。find_key给出const_iterator,所以它只能读。- 当值是
::std::size_t时,::fast_io::zu_str_btree_map/zu_str_swiss_map能省去打字。