Ch10.3: contains(): Membership Testing

中文(简体)

1. The Function You Will Use Most

contains(key) answers one question: is this key in the container? It returns bool, it never allocates, and with the default allocators it is noexcept. All six containers have it, with the same name and the same behaviour.


#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.contains("alpha"));   // 1
	println(s.contains("gamma"));   // 0
}

1
0

In a real program most of the code that touches a set looks exactly like this: a filter, a guard, a validation step. That is why contains comes first and everything else after.

2. It Takes a View, So Nothing Is Copied

The parameter is a ::fast_io::string_view — a pointer plus a length. No allocation, no copy, no temporary string. Anything that can produce a view works:

You have You write
a string literal s.contains("alpha")
a ::fast_io::string s.contains(::fast_io::string_view(k.c_str(), k.size()))
a ::fast_io::cstring_view / string_view variable s.contains(v)
a char const * that is null-terminated s.contains(::fast_io::mnp::os_c_str(p))
a slice of a larger string s.contains(line.subview(pos, len))

The last row is the one that makes contains worth having on a string set. You can test a piece of a buffer you are scanning — a token, a field between delimiters, a line minus its newline — without building a ::fast_io::string just to look it up:


::fast_io::str_btree_set dict;
dict.insert_key("the");
dict.insert_key("quick");
dict.insert_key("fox");

::fast_io::string_view line{"the quick brown fox"};
::std::size_t pos{};
while (pos < line.size())
{
	auto space{line.find(::fast_io::string_view(" "), pos)};
	if (space == ::fast_io::containers::npos)
	{
		space = line.size();
	}
	auto token{line.subview(pos, space - pos)};
	println("[", token, "] known: ", dict.contains(token));
	pos = space + 1;
}

[the] known: 1
[quick] known: 1
[brown] known: 0
[fox] known: 1
This is the difference between a lookup and a copy. A container that took a ::fast_io::string would allocate, copy the characters and free them again on every test. Here the whole scan above performs zero allocations.

3. contains vs find vs count

The three answer related questions. Choosing the right one keeps the intent of the code visible:

Call Result Use when
contains(k) bool You only need yes/no. This is the default.
find(k) / find_key(k) iterator, compare with cend() You need the element itself — its key view, its mapped value, or the position to iterate or erase from (Ch10.4).
count(k) ::std::size_t, always 0 or 1 Almost never. It is contains spelled as a number; prefer contains.

There is no way for find to fail “quietly”: the iterator either points at an element or equals cend(). If a yes/no answer is all you need, if (s.find(k) != s.cend()) is if (s.contains(k)) with more room for mistakes.

4. Patterns

Deduplicating input

Two ways to keep only the first occurrence. Either look first:


for (auto word : words)
{
	if (!seen.contains(word))
	{
		seen.insert_key(word);
		unique.push_back(word);
	}
}

or let the insert tell you, which is one lookup instead of two:


for (auto word : words)
{
	if (seen.insert_key(word))   // true only the first time
	{
		unique.push_back(word);
	}
}

Both are correct; the second is the one to prefer when you insert anyway. contains is the right tool when the container must not be modified.

Guarding with early return


void handle(::fast_io::str_swiss_set const &allowed, ::fast_io::string_view name) throws
{
	if (!allowed.contains(name))
	{
		println("rejected: ", name);
		return;
	}
	println("accepted: ", name);
}

Note the parameter: ::fast_io::str_swiss_set const & plus a view. Nothing is copied into the function call, and the const reference says “this call only reads the set”.

Membership of a map

On a map, contains asks about the key only, never the value. That is the check you need before reading a value whose key you are not sure exists:


::fast_io::str_btree_map<::std::size_t> age{{"ada", 36zu}};
println(age.contains("ada"));     // 1
println(age.contains("alan"));    // 0 — no such key

5. What “Contains” Means Exactly

The comparison is on the key bytes, exactly as given: case-sensitive, no trimming, no Unicode folding. "Alpha" and "alpha" are two different keys.

The empty string is an ordinary key. It is not in an empty container, and it can be inserted like anything else:


println(dict.contains(""));   // 0
dict.insert_key("");
println(dict.contains(""));   // 1

0
1
Comparing a view with a char const * is a compile error, on purpose. A pointer does not carry a length, so s.contains(p) cannot know what to look for. Wrap it: s.contains(::fast_io::mnp::os_c_str(p)). The error is the language protecting you from a silently truncated or runaway key, not pedantry.

6. Cost and Error Specification

contains is noexcept with the default allocators, on all six containers, and that is measured at compile time:


::fast_io::str_btree_set s;
::fast_io::str_swiss_set ss;
static_assert(noexcept(s.contains("x")));
static_assert(noexcept(ss.contains("x")));

A lookup cannot allocate (it takes a view) and cannot fail (a missing key is an answer, not an error), so there is nothing to report and nothing to handle. This is why membership tests can be called from any noexcept function, including destructors and constexpr code.

In algorithmic terms, the B-tree family compares the key along one root-to-leaf path, and the Swiss-table family hashes the key once and probes. Neither walks the elements, which is what makes a set a set.

Key takeaways