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
charstr_
wchar_twstr_
char8_tu8str_
char16_tu16str_
char32_tu32str_

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.

Copying may still throw. The constraint is only about moves. A value type with a throwing copy constructor is fine, and it simply makes the copying insert overload 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:

  1. The value type can throw where the operation constructs it. A copy constructor that can throw makes insert_key(key, lvalue) a throws function; a constructor that can throw makes the emplace_key overload that calls it throws.
  2. 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
You do not have to handle anything. With the default allocators — which is what ::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