c语言中std::map
std::map and its siblings(std::multimap, std::unordered_map/multimap) used to be my favourite containers when I was doing competitive programming. In fact, I still like them(though using less frequently nowadays). And with Modern C++, we now have more reasons to use std::map. That’s why I have decided to address this topic by writing an article summarizing these new features. So, without much gibberish, let’s dive-in directly.
在进行竞争性编程时, std :: map及其兄弟姐妹( std :: multimap , std :: unordered_map / multimap )曾经是我最喜欢的容器。 实际上,我仍然喜欢它们(尽管如今使用频率降低了)。 借助Modern C ++ ,我们现在有更多使用std :: map的理由。 这就是为什么我决定写一篇总结这些新功能的文章来解决这个问题的原因。 因此,在没有太多胡言乱语的情况下,让我们直接进行深入研究。
/!\: This article has been originally published on my blog. If you are interested in receiving my latest articles, please sign up to my newsletter.
/!\:本文最初发布在我的 博客上 。 如果您有兴趣接收我的最新文章, 请注册我的新闻通讯 。
std :: map ::包含 (C ++ 20) (std::map::contains(C++20))
std::map::contains
member function is a good step towards code expressiveness. And I am also tire of writing :std::map::contains
成员函数是朝着代码表达性迈出的重要一步。 而且我对写作也感到厌倦:
if (auto search = freq_of.find(2); search != freq_of.end()) {
cout << "Found" << endl;
}
// Where assume, freq_of = map{{3, 1}, {1, 1}, {2, 1}};
- Rather, from C++20, you can write: 相反,从C ++ 20,您可以编写:
if (freq_of.contains(2)) {
cout << "Found" << endl;
}
The code we write is written first for human consumption & only secondarily for the computer to understand.
我们编写的代码首先被编写供人类使用,其次才被计算机理解。
- John Sonmez
-约翰·桑梅兹
std :: map :: try_emplace (C ++ 17) (std::map::try_emplace(C++17))
- While inserting into the map, we have 2 different possibilities: 在插入地图时,我们有2种不同的可能性:
- The key doesn’t exist yet. Create a fresh key-value pair. 密钥尚不存在。 创建一个新的键值对。
- The key does exist already. Take the existing item and modify it. 密钥确实已经存在。 取现有项目并进行修改。
A typical approach to insert an element in
std::map
is by usingoperator[ ]
,std::map::insert
orstd::map::emplace
. But, in all of these cases, we have to bear the cost of default/specialized constructor or assignment call. And the worst part is if an item already exists, we have to drop the freshly created item.在
std::map
插入元素的典型方法是使用operator[ ]
,std::map::insert
或std::map::emplace
。 但是,在所有这些情况下,我们必须承担默认/专用构造函数或赋值调用的费用。 最糟糕的是,如果某个项目已经存在,我们就必须删除新创建的项目。
int main() {
vector v{3, 4, 5, 8, 7, 3, 5, 2, 4};
map freq_of; for (const auto &n : v) {
if (const auto &[it, inserted] = freq_of.emplace(n, 1); !inserted) {
it->second++; // Exists already
}
} assert(freq_of[3] == 2); return EXIT_SUCCESS;
}
- Instead: 代替:
if (const auto &[it, inserted] = freq_of.try_emplace(n, 1); !inserted) {
it->second++;
}
But, since C++17, there is this std::map::try_emplace method that creates items only if the key doesn’t exist yet. This boosts the performance in case objects of that type are expensive to create.
但是,从C ++ 17开始,有一个std :: map :: try_emplace方法仅在键不存在时才创建项目 。 如果创建这种类型的对象的成本很高,则可以提高性能。
Although the above example hasn’t showcased the expensive to create items. But, yes! whenever you encounter such a situation, must be known how to handle it with
std::map::try_emplace
.尽管以上示例并未展示创建项目的昂贵成本。 但是,是的! 每当遇到这种情况时,必须知道如何使用
std::map::try_emplace
处理它。
std :: map :: insert_or_assign (C ++ 17) (std::map::insert_or_assign(C++17))
When you have to insert element anyhow. For the sake of convenience, you use std::map::operator[ ]. Which is OK( and dangerous)! Unless you have any constraint on insertion or assignment.
无论如何,当您必须插入元素时。 为了方便起见,使用std :: map :: operator [] 。 可以( 危险 )! 除非您对插入或分配有任何限制。
- For example, while counting the frequency of elements with the added constraint that when an element is repeated(i.e. assigned) you have to remove all the element lesser than the current one. 例如,在计算带有附加约束的元素的频率时,当重复(即分配)一个元素时,您必须删除所有小于当前元素的元素。
In such a situation,
std::map::operator[ ]
isn't feasible. Rather,std::map::insert_or_assign
is more appropriate and returns more information thanstd::map::operator[ ]
. It also does not require default-constructibility of the mapped type. Consider the following example for the same.在这种情况下,
std::map::operator[ ]
不可行。 相反, 与std::map::operator[ ]
相比 ,std::map::insert_or_assign
更合适并且返回更多信息 。 它还不需要映射类型的默认可构造性。 同样考虑以下示例。
int main() {
vector v{8, 3, 9, 5, 8};
map freq_of; for (auto &&n : v) {
const auto &[it, is_inserted] = freq_of.insert_or_assign(n, 1); if (!is_inserted) { // remove all lesser element then current one if repeated
freq_of.erase(begin(freq_of), it);
}
} assert((freq_of == decltype(freq_of){
{8, 1},
{9, 1},
})); return EXIT_SUCCESS;
}
带提示的std :: map :: insert (C ++ 11/17) (std::map::insert With Hint(C++11/17))
Looking up items in an
std::map
takesO(log(n))
time. This is the same for inserting new items. Because the position where to insert them must looked up. Naive insertion ofM
new items would thus takeO(M * log(n))
time.在
std::map
查找项目需要O(log(n))
时间。 插入新项目的方法相同。 因为必须将它们插入的位置向上看。 因此,天真的插入M
新项将花费O(M * log(n))
时间。In order to make this more efficient,
std::map
insertion functions accept an optional insertion hint parameter. The insertion hint is basically an iterator, which points near the future position of the item that is to be inserted. If the hint is correct, then we get amortizedO(1)
insertion time.为了提高效率,
std::map
插入函数接受可选的插入提示参数。 插入提示基本上是一个迭代器,它指向要插入的项目的将来位置。 如果提示正确,那么我们将摊销O(1)
插入时间。- This is quite useful from a performance point of view when the insertion sequence of items is somewhat predictable. For example: 从性能的角度来看,当项目的插入顺序是可预测的时,这是非常有用的。 例如:
int main() {
map m{{2, ""}, {3, ""}};
auto where(end(m)); for (const auto &n : {8, 7, 6, 5, 4, 3, 2, 1}) { // Items in non-incremental order
where = m.insert(where, {n, ""});
} // How it is not done!
// m.insert(end(m), {0, ""}); for (const auto &[key, value] : m) {
cout << key << " : " << value << endl;
} return EXIT_SUCCESS;
}
A correct hint will point to an existing element, which is greater than the element to be inserted so that the newly inserted key will be just before the hint. If this does not apply for the hint the user provided during insertion, the insert function will fall back to a nonoptimized insertion, yielding
O(log(n))
performance again.正确的提示将指向一个现有元素,该元素大于要插入的元素,因此新插入的键将刚好在提示之前。 如果这不适用于用户在插入过程中提供的提示,则插入功能将退回到未优化的插入状态,从而再次产生
O(log(n))
性能。- For the above example, the first insertion, we got the end iterator of the map, because we had no better hint to start with. After installing an 8 in the tree, we knew that installing 7 will insert a new item just in front of the 8, which qualified it to be a correct hint. This applies to 6 as well, if put into the tree after inserting the 7, and so on. This is why it is possible to use the iterator, which was returned in the last insertion for the next insertion. 对于上面的示例,第一次插入,我们获得了地图的结束迭代器,因为我们没有更好的提示开始。 在树中安装8后,我们知道安装7将在8的前面插入一个新项,这将其限定为正确的提示。 如果在插入7之后将其放入树中,则这也适用于6。 这就是为什么可以使用在上一次插入中返回的迭代器进行下一次插入的原因。
You can play around the above example to justify the performance gain with quick-benchmark.
您可以绕过上面的示例,以使用quick-benchmark证明性能提高是合理的。
*Note:* It is important to know that before C++11, insertion hints were considered correct when they pointed before the position of the newly inserted item.
*注意: *重要的是要知道,在C ++ 11之前,插入提示指向新插入项的位置之前被认为是正确的。
std :: map :: merge (C ++ 17) (std::map::merge(C++17))
Same as std::list:splice, which transfers the elements from one list to another. we have
std::map::merge
which can merge the two same type ofstd::map
.与std :: list:splice相同 ,后者将元素从一个列表传输到另一个列表。 我们有
std::map::merge
可以合并两种相同类型的std::map
。
int main() {
map fruits{{5, "grapes"}, {2, "tomoto"}};
map person{{2, "mickel"}, {10, "shree"}};
map fruits_and_persons; fruits_and_persons.merge(fruits);
assert(fruits.size() == 0); fruits_and_persons.merge(person);
assert(person.size() == 1);
assert(person.at(2) == "mickel"); // Won't overwrite value at 2 i.e.`mickel` assert((fruits_and_persons == decltype(fruits){
{2, "tomoto"},
{5, "grapes"},
{10, "shree"},
})); return EXIT_SUCCESS;
}
The thing here to note is what happens when there are duplicates! The duplicated elements are not transferred. They’re left behind in the right-hand-side map.
这里要注意的是,当重复时会发生什么! 重复的元素不会传输。 他们被遗留在右侧地图中 。
std :: map :: extract (C ++ 17 (std::map::extract(C++17)
Unlike
std::map::merge
that transfers the elements in bulk,std::map::extract
along withstd::map::insert
transfers element piecewise. But what is the more compelling application ofstd::map::extract
is modifying keys.与
std::map::merge
大量传输元素不同,std::map::extract
与std::map::insert
逐段传输元素 。 但是std::map::extract
更引人注目的应用是修改密钥。As we know, for
std::map
keys are always unique and sorted. Hence, It is crucial that users cannot modify the keys of map nodes that are already inserted. In order to prevent the user from modifying the key items of perfectly sorted map nodes, the const qualifier is added to the key type.众所周知,对于
std::map
键始终是唯一的且已排序。 因此,至关重要的是用户不能修改已插入的地图节点的键。 为了防止用户修改完全排序的地图节点的关键项,将const限定符添加到关键类型中。This kind of restriction is perfectly valid because it makes harder for the user to use
std::map
the wrong way. But what if we really need to change the keys of some map items?这种限制是完全有效的,因为它使用户更难以错误的方式使用
std::map
。 但是,如果我们真的需要更改某些地图项的键怎么办?- Prior to C++17, we had to remove & reinsert the items in order to change the key. The downside of this approach is memory allocation & deallocation, which sounds bad in terms of performance. But, from C++17, we can remove & reinsert std::map nodes without any reallocation of memory. 在C ++ 17之前,我们必须删除并重新插入项目才能更改密钥。 这种方法的缺点是内存分配和释放,这在性能方面听起来很糟糕。 但是,从C ++ 17开始,我们可以删除并重新插入std :: map节点,而无需重新分配内存。
int main() {
map race_scoreboard{{1, "Mickel"}, {2, "Shree"}, {3, "Jenti"}};
using Pair = map::value_type; {
auto Jenti(race_scoreboard.extract(3));
auto Mickel(race_scoreboard.extract(1)); swap(Jenti.key(), Mickel.key()); auto [it, is_inserted, nh] = race_scoreboard.insert(move(Jenti)); // nh = node handle
assert(*it == Pair(1, "Jenti") && is_inserted == true && nh.empty()); race_scoreboard.insert(move(Mickel));
} assert((race_scoreboard == decltype(race_scoreboard){
{1, "Jenti"},
{2, "Shree"},
{3, "Mickel"},
})); return EXIT_SUCCESS;
}
Consider the above example of the racing scoreboard where you have employed
std::map
to imitate the racing position. And after a while, Jenti took the lead & Mickel left behind. In this case, how we have switched the keys(position on a race track) of those players.考虑上面的竞赛记分牌示例,其中您使用了
std::map
来模仿竞赛位置。 过了一会儿,简蒂(Jenti)带头,米克尔(Mickel)落伍了。 在这种情况下,我们如何切换这些玩家的按键(在赛道上的位置)。std::map::extract
comes in two flavours:std::map::extract
有两种口味:
node_type extract(const_iterator position);
node_type extract(const key_type& x);
- In the above example, we used the second one, which accepts a key and then finds & extracts the map node that matches the key parameter. The first one accepts an iterator, which implies that it is faster because it doesn’t need to search for the item. 在上面的示例中,我们使用了第二个示例,该示例接受一个键,然后查找并提取与键参数匹配的地图节点。 第一个接受迭代器,这意味着它更快,因为它不需要搜索项目。
如果不存在具有特定密钥的节点怎么办? (What If the Node With a Particular Key Does Not Exist?)
If we try to extract an item that doesn’t exist with the second method (the one that searches using a key), it returns an empty
node_type
instance i.e. node handle. Theempty()
member method or overloaded bool operator tells us that whether anode_type
instance is empty or not.如果我们尝试提取第二种方法(使用键进行搜索的方法)不存在的项目,则它将返回一个空的
node_type
实例,即node handle 。empty()
成员方法或重载的布尔运算符告诉我们node_type
实例是否为空。
好! 然后,如何修改std :: map键? (OK! Then How Do I Modify std::map Keys?)
After extracting nodes, we were able to modify their keys using the
key()
method, which gives us non-const access to the key, although keys are usually const.提取节点后,我们可以使用
key()
方法修改其键,尽管键通常是const ,但这使我们可以非常量访问键。Note that in order to reinsert the nodes into the map again, we had to move them into the insert function. This makes sense because the extract is all about avoiding unnecessary copies and allocations. Moreover, while we move a
node_type
instance, this does not result in actual moves of any of the container values.请注意,为了再次将节点重新插入到映射中,我们必须将其移动到insert函数中。 这是有道理的,因为摘录只是为了避免不必要的复制和分配。 而且,虽然我们移动一个
node_type
实例,但这不会导致任何容器值的实际移动。
我还可以在std :: map中修改关联值吗? (Can I Modify Associated Values in std::map Also?)
Yes! You can use the accessor methods
nh.mapped()
(instead ofnh.key()
) to manipulate the pieces of the entry in astd::map
(ornh.value()
for the single piece of data in an element of astd::set
). Thus you can extract, manipulate, and reinsert a key without ever copying or moving its actual data.是! 您可以使用访问器方法
nh.mapped()
(而不是nh.key()
)来处理std::map
中的条目项(或nh.value()
来处理元素中的单个数据项)。一个std::set
)。 因此,您可以提取,操作和重新插入密钥,而无需复制或移动其实际数据。
但是安全呢? (But What About Safety?)
If you extract a node from a map and then throw an exception before you’ve managed to re-insert it into the destination map.
如果您从地图提取节点,然后在设法将其重新插入到目标地图之前抛出异常 。
A node handle’s destructor is called and will correctly clean up the memory associated with the node. So, technically
std::map::extract
by-default(without insert) will act as std::map::erase!节点句柄的析构函数将被调用,并将正确清理与该节点关联的内存。 因此,从技术上讲,默认情况下
std::map::extract
(不带插入)将充当 std :: map :: erase !
还有更多! 互通性 (There Is More! Interoperability)
Map nodes that have been extracted using the
std::map::extract
are actually very versatile. We can extract nodes from a map instance and insert it into any other map or even multimap instance.使用
std::map::extract
地图节点实际上非常通用。 我们可以从地图实例中提取节点,然后将其插入任何其他地图甚至多地图实例中 。It does also work between unordered_map and unordered_multimap instances, as well as with set/multiset and respective unordered_set/unordered_multiset.
它也可以在unordered_map和unordered_multimap实例之间以及set / multiset和相应的unordered_set / unordered_multiset之间工作 。
- In order to move items between different map/set structures, the types of key, value and allocator need to be identical. 为了在不同的映射/集合结构之间移动项目,键,值和分配器的类型必须相同。
运算符[]与insert()与at()之间的区别 (Difference Between operator[ ] vs insert() vs at())
This is trivial for experienced devs but, still I want to go over it quickly.
对于经验丰富的开发人员而言,这是微不足道的,但是,我仍然想快速了解一下。
std :: map :: operator [] (std::map::operator[ ])
Operation: find-or-add; try to find an element with the given key inside the map, and if it exists it will return a reference to the stored value. If it does not, it will create a new element inserted in place with default initialization and return a reference to it.
操作 :查找或添加; 尝试在地图中查找具有给定键的元素,如果该元素存在,它将返回对存储值的引用。 如果没有,它将创建一个默认插入的新元素,并返回对其的引用。
Applicability:
适用范围
Not usable for
const std::map
, as it will create the element if it doesn't exist.不适用于
const std::map
,因为如果不存在,它将创建元素。- Not suitable for value type that does not default constructible and assignable(in layman term, doesn’t have default constructor & copy/move constructor). 不适合不能默认构造和分配的值类型(用外行术语来说,没有默认构造函数和复制/移动构造函数)。
When key exists: Overwrites it.
当密钥存在时 :覆盖它。
std :: map :: insert (std::map::insert)
Operation: insert-or-nop; accepts a value_type (
std::pair
) and uses the key(first member) and to insert it. Asstd::map
does not allow for duplicates, if there is an existing element it will not insert anything.操作 :插入或插入; 接受一个value_type(
std::pair
)并使用key(first成员)并将其插入。 由于std::map
不允许重复,因此如果存在现有元素,则不会插入任何内容。Applicability:
适用范围
- Liberty in calling insert different ways that require the creation of the value_type externally and the copy of that object into the container. 调用时的自由以不同的方式插入,这需要在外部创建value_type并将该对象的副本复制到容器中。
- Highly applicable when item insertion sequence is somewhat predictable to gain the performance. 当物品插入顺序在某种程度上可以预测以提高性能时,则非常适用。
When key exists: Not modify the state of the map, but instead return an iterator to the element that prevented the insertion.
当key存在时 :不修改映射的状态,而是将迭代器返回到阻止插入的元素。
std :: map :: at (std::map::at)
Operation: find-or-throw; returns a reference to the mapped value of the element with key equivalent to input key. If no such element exists, an exception of type std::out_of_range is thrown.
操作 :查找或抛出; 使用与输入键等效的键返回对元素映射值的引用。 如果没有这样的元素存在,一个异常类型的std :: out_of_range异常。
Applicability:
适用范围
Not recommended using
at()
when accessing const maps and when element absence is a logic error.当访问常量映射并且元素缺失是逻辑错误时,不建议使用
at()
。Yes, it’s better to use
std::map::find()
when you're not sure element is there. Because, throwing and catching std::logic_error exception will not be a very elegant way of programming, even if we don't think about performance.是的,当您不确定元素在那里时,最好使用
std::map::find()
。 因为,即使我们不考虑性能,抛出并捕获std :: logic_error异常也不是一种非常优雅的编程方式。When key exists: returns a reference to mapped value.
当key存在时 :返回对映射值的引用。
分词 (Parting Words)
If you see the table of content for this article above, more than half of the member functions are around inserting the elements into the map. To the newbie, this is the reason for anxiety(or standard committee would say modernness). But if you account for the new features & complexity of language those are pretty much justified. BTW, this modernness doesn’t stop here, we do have other specialization also available for map like std::swap(C++17), std::erase_if(C++20) & bunch of comparison operators.
如果您在上面看到了本文的目录,那么一半以上的成员函数就是将元素插入到地图中。 对于新手来说,这就是焦虑的原因(或者标准委员会会说现代性)。 但是,如果您考虑到语言的新功能和复杂性,那么这是很合理的。 顺便说一句,这种现代性不止于此,我们还为地图提供了其他特殊功能,例如std :: swap (C ++ 17), std :: erase_if (C ++ 20)和一堆比较运算符。
翻译自: https://medium.com/dev-genius/using-std-map-wisely-with-modern-c-cdc7c2c81b52
c语言中std::map