Skip to content

Commit 2293f9c

Browse files
committed
flat_map: last remaing API bits
1 parent aa03cac commit 2293f9c

3 files changed

Lines changed: 383 additions & 1 deletion

File tree

container/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Algorithms, adapters, and containers for STL-compatible code.
88
|---|---|
99
| `algorithms.hpp` | `ContainerAlgorithms::erase_if()` applies the remove/erase idiom to sequence containers. |
1010
| `chunked_deque.hpp` | `chunked_deque` holds elements in fixed-size blocks, each with an occupancy bitmask, so an erasure anywhere in the sequence clears a bit instead of relocating anything. Push and pop at both ends, insertion anywhere, and stable references. `operator[]` is linear in the block count: uneven occupancy rules out index arithmetic. `reserve()` pre-allocates blocks and keeps them across a drain. |
11-
| `flat_map.hpp` | Vector-backed sorted `flat_map` and `flat_set` with heterogeneous lookup, forward and reverse random-access iteration, ordinary insertion/erasure, sorted-range merging, and batched unsorted insertion. |
11+
| `flat_map.hpp` | Vector-backed sorted `flat_map` and `flat_set` with heterogeneous lookup, forward and reverse random-access iteration, ordinary insertion/erasure, sorted-range merging, batched unsorted insertion, and whole-vector handover in and out. |
1212
| `iterator_helpers.hpp` | `const_forward_iterator_wrapper` retains both an iterator and its parent container, so `endReached()` needs no second iterator. `isBound()` reports whether a container was supplied, not whether the iterator is still valid. Factories provide wrapped `cbegin`/`cend`. |
1313
| `multi_index.hpp` | `MultiIndexSet` owns values uniquely by one member and maintains a non-unique secondary-member index with exact and range lookup. |
1414
| `multimap_helpers.hpp` | `multimap_value_iterator` adapts a multimap iterator to expose only its mapped value while retaining access to the native iterator. |
@@ -24,6 +24,10 @@ Algorithms, adapters, and containers for STL-compatible code.
2424

2525
`keys()` returns the backing key vector as a const reference, and `flat_map::values()` the mapped vector, indexed in lockstep with it and with iteration. Both are const: a write through a mutable handle would break the sort order, or desynchronize the map's two vectors.
2626

27+
Whole vectors also move in and out without an element-by-element pass. `std::move(container).extract()` empties the container and returns its storage with capacity intact: a `containers` aggregate of `keys` and `values` for the map, the key vector itself for the set. `replace()` and the vector constructors take storage back. The `sorted_unique` overloads adopt it untouched and assert the caller's ordering and uniqueness; the untagged ones sort and deduplicate in place. `replace()` keeps the container's comparator, which assigning a whole new container would replace.
28+
29+
`merge()` folds another container of the same type in, moving its entries across in one pass and leaving it empty. Existing entries win the collisions, as they do when a batch ends, and unlike `std::map::merge` the colliding entries are dropped rather than left behind in the source.
30+
2731
Both flat containers support ordinary insertion, merging a sorted range, inserting a range in any order, `append_sorted_unique()`, and batched unsorted appends. Between `begin_batch()` and `end_batch()`, ordered operations and iteration are invalid. Finalization sorts only the appended tail and merges it with the existing prefix. A tail or sorted range starting past the last existing key skips the merge entirely, so growing a container by repeated appends never rebuilds it. Existing entries win conflicts with a batch, and the first batch entry wins duplicates within that batch. `insert(first, last)` and the initializer-list `insert()` sort and merge a range under those same rules, leaving the container unchanged if constructing an element throws. `insert_sorted()` is the cheaper path when the range is known to be ordered. `abort_batch()` instead discards the appended tail and restores the state from before `begin_batch()`; `clear()` ends an open batch by discarding the container. A failed `end_batch()` leaves the batch open, so `abort_batch()` remains available and lookups keep asserting until it runs.
2832

2933
Key equality uses `operator==` when the compared types provide it and comparator equivalence otherwise. When both are available, they must describe the same equivalence relation.

container/flat_map.hpp

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ namespace FlatContainerInternal {
3939
return !compare(query, stored_key); // lower_bound already established !(stored_key < query)
4040
}
4141

42+
template <typename Key, typename Compare>
43+
[[nodiscard]] bool sorted_and_unique(const std::vector<Key>& keys, const Compare& compare)
44+
{
45+
return std::is_sorted(keys.begin(), keys.end(), compare)
46+
&& std::adjacent_find(keys.begin(), keys.end(),
47+
[&](const Key& left, const Key& right) { return sorted_keys_equal(left, right, compare); }) == keys.end();
48+
}
49+
4250
template <typename T>
4351
concept SynthThreeWayComparable = std::three_way_comparable<T> || requires(const T& left, const T& right) {
4452
{ left < right } -> std::convertible_to<bool>;
@@ -116,6 +124,10 @@ namespace FlatContainerInternal {
116124

117125
} // namespace FlatContainerInternal
118126

127+
// Marks the overloads that adopt a caller's vectors as they are: ordered by the container's comparator, no duplicate keys
128+
struct sorted_unique_t { explicit sorted_unique_t() = default; };
129+
inline constexpr sorted_unique_t sorted_unique{};
130+
119131
template <typename Key, typename Mapped, typename Compare = std::less<>>
120132
class flat_map
121133
{
@@ -240,6 +252,13 @@ class flat_map
240252
using reference = typename iterator::reference;
241253
using const_reference = typename const_iterator::reference;
242254

255+
// The storage extract() hands over and replace() takes back, index-aligned as keys() and values() are
256+
struct containers
257+
{
258+
std::vector<Key> keys;
259+
std::vector<Mapped> values;
260+
};
261+
243262
flat_map() = default;
244263
explicit flat_map(Compare compare): _compare(std::move(compare)) {}
245264

@@ -248,6 +267,22 @@ class flat_map
248267

249268
flat_map(std::initializer_list<value_type> values, Compare compare = {}): _compare(std::move(compare)) { insert(values); }
250269

270+
// Adopts the vectors untouched: the caller's ordering and uniqueness are asserted, never established
271+
flat_map(sorted_unique_t, std::vector<Key> keys, std::vector<Mapped> values, Compare compare = {})
272+
: _keys(std::move(keys)), _values(std::move(values)), _compare(std::move(compare))
273+
{
274+
assert(_keys.size() == _values.size());
275+
assert(FlatContainerInternal::sorted_and_unique(_keys, _compare));
276+
}
277+
278+
// Sorts and deduplicates in place: the entries never leave the vectors handed in
279+
flat_map(std::vector<Key> keys, std::vector<Mapped> values, Compare compare = {})
280+
: _keys(std::move(keys)), _values(std::move(values)), _compare(std::move(compare))
281+
{
282+
assert(_keys.size() == _values.size());
283+
merge_appended_tail(0);
284+
}
285+
251286
[[nodiscard]] bool empty() const noexcept { return _keys.empty(); }
252287
[[nodiscard]] size_type size() const noexcept { return _keys.size(); }
253288
[[nodiscard]] bool batch_open() const noexcept { return _batch_start != no_batch; }
@@ -307,6 +342,49 @@ class flat_map
307342
[[nodiscard]] const std::vector<Key>& keys() const noexcept { assert_not_batching(); return _keys; }
308343
[[nodiscard]] const std::vector<Mapped>& values() const noexcept { assert_not_batching(); return _values; }
309344

345+
// Empties the container: the vectors leave with their capacity, ready to be reworked and handed back
346+
[[nodiscard]] containers extract() &&
347+
{
348+
assert_not_batching();
349+
containers extracted{ std::move(_keys), std::move(_values) };
350+
clear();
351+
return extracted;
352+
}
353+
354+
// Keeps this container's comparator, which assigning a whole new container would replace
355+
void replace(sorted_unique_t, std::vector<Key>&& keys, std::vector<Mapped>&& values)
356+
{
357+
assert_not_batching();
358+
assert(keys.size() == values.size());
359+
assert(FlatContainerInternal::sorted_and_unique(keys, _compare));
360+
_keys = std::move(keys);
361+
_values = std::move(values);
362+
}
363+
364+
void replace(std::vector<Key>&& keys, std::vector<Mapped>&& values)
365+
{
366+
assert_not_batching();
367+
assert(keys.size() == values.size());
368+
_keys = std::move(keys);
369+
_values = std::move(values);
370+
// The whole container is the batch: a throw from the comparator then leaves it open for abort_batch()
371+
_batch_start = 0;
372+
end_batch();
373+
}
374+
375+
// Consumes other entirely: a colliding entry is dropped, not left behind as std::map::merge leaves it
376+
// Entries already here win a collision, as they do when a batch ends
377+
void merge(flat_map&& other)
378+
{
379+
assert(this != &other);
380+
assert_not_batching();
381+
other.assert_not_batching();
382+
assert(FlatContainerInternal::sorted_and_unique(other._keys, _compare));
383+
// Emptying other up front keeps it empty even when the merge throws partway through moving the entries out
384+
auto [keys, values] = std::move(other).extract();
385+
merge_sorted(std::move(keys), std::move(values));
386+
}
387+
310388
// Ends an open batch as well
311389
void clear() noexcept
312390
{
@@ -873,6 +951,18 @@ class flat_set
873951

874952
flat_set(std::initializer_list<Key> values, Compare compare = {}): _compare(std::move(compare)) { insert(values); }
875953

954+
// Adopts the vector untouched: the caller's ordering and uniqueness are asserted, never established
955+
flat_set(sorted_unique_t, std::vector<Key> keys, Compare compare = {}): _keys(std::move(keys)), _compare(std::move(compare))
956+
{
957+
assert(FlatContainerInternal::sorted_and_unique(_keys, _compare));
958+
}
959+
960+
// Sorts and deduplicates in place: the keys never leave the vector handed in
961+
explicit flat_set(std::vector<Key> keys, Compare compare = {}): _keys(std::move(keys)), _compare(std::move(compare))
962+
{
963+
merge_appended_tail(0);
964+
}
965+
876966
[[nodiscard]] bool empty() const noexcept { return _keys.empty(); }
877967
[[nodiscard]] size_type size() const noexcept { return _keys.size(); }
878968
[[nodiscard]] bool batch_open() const noexcept { return _batch_start != no_batch; }
@@ -914,6 +1004,44 @@ class flat_set
9141004
// Const only: a write through a mutable handle could break the sort order
9151005
[[nodiscard]] const std::vector<Key>& keys() const noexcept { assert_not_batching(); return _keys; }
9161006

1007+
// Empties the container: the vector leaves with its capacity, ready to be reworked and handed back
1008+
[[nodiscard]] std::vector<Key> extract() &&
1009+
{
1010+
assert_not_batching();
1011+
auto extracted = std::move(_keys);
1012+
clear();
1013+
return extracted;
1014+
}
1015+
1016+
// Keeps this container's comparator, which assigning a whole new container would replace
1017+
void replace(sorted_unique_t, std::vector<Key>&& keys)
1018+
{
1019+
assert_not_batching();
1020+
assert(FlatContainerInternal::sorted_and_unique(keys, _compare));
1021+
_keys = std::move(keys);
1022+
}
1023+
1024+
void replace(std::vector<Key>&& keys)
1025+
{
1026+
assert_not_batching();
1027+
_keys = std::move(keys);
1028+
// The whole container is the batch: a throw from the comparator then leaves it open for abort_batch()
1029+
_batch_start = 0;
1030+
end_batch();
1031+
}
1032+
1033+
// Consumes other entirely: a colliding key is dropped, not left behind as std::set::merge leaves it
1034+
// Keys already here win a collision, as they do when a batch ends
1035+
void merge(flat_set&& other)
1036+
{
1037+
assert(this != &other);
1038+
assert_not_batching();
1039+
other.assert_not_batching();
1040+
assert(FlatContainerInternal::sorted_and_unique(other._keys, _compare));
1041+
// Emptying other up front keeps it empty even when the merge throws partway through moving the keys out
1042+
merge_sorted(std::move(other).extract());
1043+
}
1044+
9171045
// Ends an open batch as well
9181046
void clear() noexcept { _keys.clear(); _batch_start = no_batch; }
9191047
void reserve(size_type count) { _keys.reserve(count); }

0 commit comments

Comments
 (0)