Ch10.11: Element Types, Error Specifications, and Validity
1. What the Key May Be
The key type parameter is constrained to integral character types. In practice that means the five the aliases cover:
| Type | Alias prefix |
|---|---|
char | str_ |
wchar_t | wstr_ |
char8_t | u8str_ |
char16_t | u16str_ |
char32_t | u32str_ |
A key is a sequence of those units, compared byte by byte. There is no
encoding awareness: a u32str_btree_map compares code points,
and a str_ container compares UTF-8 bytes. For UTF-8, byte-wise
ordering happens to agree with code point ordering, which is why the
str_ family is the right default even for UTF-8 text.
2. What the Mapped Type May Be
Map values must be nothrow move constructible and nothrow move assignable. This is a class-level constraint, checked the moment you name the type:
error: template constraint failure for
'template<class chtype, class mappedtype, class Allocator, long unsigned int keys_number>
requires (integral<chtype>) && (keys_number != 0 && (keys_number & 1) == 0 &&
(is_nothrow_move_constructible_v<mappedtype>) &&
(is_nothrow_move_assignable_v<mappedtype>))
class fast_io::containers::basic_str_btree_map'
note: constraints not satisfied
The reason is the same one behind every fast_io container: when
a node splits, values are relocated. A relocation that can fail would leave
the container in a half-moved state with no way to describe it, so the
requirement is moved to compile time. Types with a noexcept
move — ::std::size_t, ::fast_io::string,
::fast_io::vector, most of what you will store — satisfy it
automatically.
throws — see Section 3.
3. Error Specifications
With the default allocators, a container of nothrow-movable values is
entirely noexcept: every insert, every erase,
every lookup, every iterator operation. You can verify that at compile time
instead of trusting the documentation:
::fast_io::str_btree_map<::std::size_t> m;
static_assert(noexcept(m.insert_key("x", 1zu)));
static_assert(noexcept(m.insert_key_or_assign("x", 1zu)));
static_assert(noexcept(m.emplace_key("x", 1zu)));
static_assert(noexcept(m.find("x")));
static_assert(noexcept(m.erase_key("x")));
static_assert(noexcept(m.clear()));
A failed allocation in that configuration terminates the program; it does not throw. That is the deliberate default: running out of memory is not a condition a string container can recover from, so the runtime does not pretend otherwise.
Two things can make a member throws instead, and both are
visible in the type, not in a comment:
-
The value type can throw where the operation constructs it.
A copy constructor that can throw makes
insert_key(key, lvalue)athrowsfunction; a constructor that can throw makes theemplace_keyoverload that calls itthrows. - You chose a throwing allocator. Wrapping the allocator with flags turns allocation failure and contract violations into herbceptions.
using throwing_alloc = ::fast_io::generic_allocator_adapter<
::fast_io::native_global_allocator,
::fast_io::allocator_adapter_flags::throws_on_allocation_failure |
::fast_io::allocator_adapter_flags::throws_on_violations>;
::fast_io::containers::basic_str_btree_set<char, throwing_alloc> ts;
static_assert(!noexcept(ts.insert_key("x"))); // may throw on allocation failure
static_assert(!noexcept(ts.front())); // may throw on an empty container
The interesting part is what a checked operation reports. Reading
front() of an empty container is a bug in your program, not a
runtime condition — but if you asked for errors to be reportable, it is
reported as ::std::errc::invalid_argument in the POSIX domain:
try
{
perrln(ts.front()); // ts is empty
}
catch throws (::std::error e)
{
perrln("caught");
perrln(e);
}
caught
[posix](22)Invalid argument
::fast_io::str_btree_map and
friends use — the containers are noexcept and there is no
try/catch throws anywhere in your code. Throwing
allocators exist for the programs that need a recoverable path, and they must
be opted into explicitly.
4. Writing Your Own Signatures
Because the specifications are conditional, the rule from
Ch6.16 does the work for
you: a function that only reads from a container is noexcept, and
a function that inserts is noexcept unless the container can
throw.
// reads only: contains/find/iteration are noexcept
bool is_known(::fast_io::str_btree_set const &s, ::fast_io::string_view w) noexcept
{
return s.contains(w);
}
// inserts: still noexcept with the default allocator
void remember(::fast_io::str_btree_set &s, ::fast_io::string_view w) noexcept
{
s.insert_key(w);
}
When the mapped type can throw, the compiler refuses the second shape and tells you why:
error: an unhandled herbceptions failure cannot leave a function without a
herbceptions specification
Mark that function throws and the error goes away — there is
never a reason to fall back to legacy exceptions or to
catch (...).
5. What Stays Valid
Iterators, key views and mapped references point into the container’s own storage. The rules below are short, and following them is the whole of memory safety with these types.
| Operation | Effect on iterators and views |
|---|---|
Lookup (contains, find, lower_bound, iteration) |
Nothing is invalidated. Lookups only read. |
| Insert (B-tree family) | The inserted element’s own iterator is new; existing keys keep their identity, so views of other elements survive. |
| Insert (Swiss family) | A growth rehashes every key. Treat all iterators and views as gone once the table grows. |
| Erase | Everything referring to the erased element dies. Only the returned iterator is known-good. |
clear(), clear_destroy(), destruction |
Everything dies. |
| Move, copy-assign, swap | Iterators into the moved-from container are gone; re-look the keys up. |
In practice: do not hold an iterator or a key view across a modification. Look the key up again — the lookup is the cheap operation you chose this container for in the first place.
// safe: the view is used immediately
println(s.contains("alpha"));
// unsafe: the view outlives the modification it was taken before
::fast_io::cstring_view kept{*s.find("alpha")};
s.insert_key("something else");
// kept may be dangling here
Keys that are copied out are of course fine forever — a
::fast_io::string built from e.key() is your own
storage and has nothing to do with the container any more.
Key takeaways
- Keys are integral character types:
char,wchar_t,char8_t,char16_t,char32_t; comparison is byte-wise. - Mapped values must be nothrow-move-constructible and nothrow-move-assignable, enforced by the class constraint.
- With default allocators, every container operation is
noexcept; allocation failure terminates. - A throwing value operation, or a throwing allocator adapter, makes exactly those members
throws— never the whole container. - Contract violations (
front()on empty, out-of-rangenth_element) terminate, or throw::std::errc::invalid_argumentin a throwing configuration. - Functions that only read are
noexcept; inserters arenoexceptunless the value type or allocator can throw, and then they arethrows. - Never keep an iterator or key view across a modification; look the key up again.