Ch10.6: Erasing

中文(简体)

1. Erasing by Key

erase_key(key) removes the element whose key you name and returns how many elements it removed — 1 or 0:


#include <fast_io_dsal/str_btree_set.h>
#include <fast_io.h>

using namespace ::fast_io::io;

int main()
{
	::fast_io::str_btree_set s;
	s.insert_key("alpha");
	s.insert_key("beta");

	println(s.erase_key("alpha"));   // 1: removed
	println(s.erase_key("alpha"));   // 0: nothing to remove
	println(s.erase_key("nope"));    // 0
}

1
0
0

Erasing a key that is not there is not an error. If you need to know whether it was there, look at the return value, or call contains first when the branch is easier to read that way. The removal itself never needs an iterator and never allocates.

2. Erasing by Iterator

erase(it) removes the element the iterator points at and returns an iterator to the next element — the element that took the removed one’s place in the order:


auto it{s.find("beta")};
it = s.erase(it);          // it now points at the element after "beta"
if (it != s.cend())
{
	println("next: ", *it);
}

next: c

Returning the next position is what makes the erase-while-iterating loop safe without incrementing anything by hand. The loop below removes every key that starts with "tmp":


for (auto it{s.cbegin()}; it != s.cend();)
{
	::fast_io::string_view k{*it};
	if (k.starts_with(::fast_io::string_view("tmp")))
	{
		it = s.erase(it);   // already advanced
	}
	else
	{
		++it;
	}
}
Never ++it after erase(it). The iterator you passed in is invalid the moment the element dies; only the returned iterator is usable. Writing s.erase(it); ++it; is a use-after-free that will not be caught for you.

Erasing a range

erase(first, last) removes the left-inclusive range [first, last) — every element from first up to, but not including, last — and returns an iterator to the element last pointed at:


auto first{s.find("c")};
auto last{s.find("d")};
auto after{s.erase(first, last)};   // removes "c" only
println("next: ", *after);          // d

next: d

With last == cend() everything from first to the end is removed, and the returned iterator is cend().

3. Erasing Everything


s.clear();                  // remove all elements, keep the object usable
println(s.is_empty());      // 1
s.insert_key("again");      // fine

s.clear_destroy();          // remove all elements and release the container's storage

Both are noexcept and both are safe on an empty container. After clear() the following inserts behave exactly as in a fresh container; clear_destroy() additionally drops whatever the container had allocated for its own bookkeeping, which is what you want before letting a large container sit idle.

4. One Statement, One Mutation

C++ does not specify the order in which function arguments are evaluated. That turns this line into a bug:


println(s.erase_key("beta"), s.size());   // WRONG: size() may run before the erase

Whether you get the size before or after the removal depends on the compiler. Split mutation and inspection into separate statements and the question disappears:


auto n{s.erase_key("beta")};
println("removed ", n, ", size now ", s.size());
This is not specific to containers. Any call that both mutates and reports state runs into the same problem. The style rule “one statement, one mutation” costs nothing and removes the whole category.

5. What Erasing Invalidates

Erasing destroys an element, so everything that referred to that element is dead: its iterator, and any key view or mapped reference you took from it. Views taken from other elements are not affected by an erase in the B-tree family, because inserting and erasing moves whole subtrees rather than shuffling elements around.

In the Swiss-table family, an erase can move an element into the hole it leaves behind, so treat any reference into the table as gone once you modify it. The rule that always holds, for all six containers:

Key takeaways