第10.7章: 映射:带值的键

English

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
B 树映射给出的是一个小代理对象;Swiss 映射给出的是引用。这就是为什么上面 B 树的循环写 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;
		}
	}
}

要点