Library

svector v1.4.0

Versionv1.4.0
Stars★ 120
Released2026-08-05

Compact SVO optimized vector for C++17 or higher

Release notes

A conformance release. `svector` now answers the same things `std::vector` answers about its own size limits, it has the C++23 range members, and a branch of the allocator support that 1.3.0 shipped without a test has one.

The one byte of overhead in direct mode is unchanged: `svector<uint8_t, 1>` is still 8 bytes holding 7.

## Changed

- **`max_size()` counts elements rather than bytes.** It used to answer `PTRDIFF_MAX` for every `T`, which claimed a size no `svector` could reach — allocation has always refused anything whose bytes pass `PTRDIFF_MAX`, so the real ceiling was `PTRDIFF_MAX / sizeof(T)` all along. That is now what it says, and it is what `std::vector` says. For `svector<int>` it goes from 9223372036854775807 to 2305843009213693951. Still `static`, for the reason the comment there gives.

- **Asking for more than that throws `std::length_error`, not `std::bad_alloc`.** These are different questions and `std::vector` spells them differently: a size that cannot exist is a `length_error`; a size that can but that the allocator will not give is a `bad_alloc`. So `reserve(max_size())` is still a `bad_alloc` — that size is legal and the allocation is what fails — while one past it is now a `length_error`.

  **This is the one thing to check before upgrading.** If you catch `std::bad_alloc` around a `reserve`, `resize` or `insert` that might be handed an unreasonable count, that handler no longer runs; `std::length_error` derives from `std::logic_error`, not from `std::bad_alloc`. Catching `std::exception` is unaffected.

## Added

- **The C++23 range members**: `assign_range`, `append_range`, `insert_range`, and the `std::from_range` constructor. The README claimed `svector` implements all of `std::vector`'s API, and since libstdc++ grew these that had quietly stopped being true.

  All four go through the iterator pair members, so a range gets the same growth, the same exception guarantees and the same self-referencing checks an iterator pair already got, rather than a second implementation of all three. A range that cannot be handed over as a pair — one whose sentinel is not its iterator, or whose iterator publishes no `iterator_category`, both of which are true of `views::filter` over `views::iota` — is built into a temporary first. That costs an allocation for exactly the ranges that could not have been sized anyway.

  They are guarded on `__cpp_lib_containers_ranges` rather than on the language version, because what they need is `std::from_range_t`. **A C++17 build is exactly what it was.**

## Tests

126 cases at C++17 and C++20, 131 at C++23, plus a replay of the 1651 entry fuzzing corpus.

Coverage over the header, aggregated across template instantiations, had nine source lines that no test reached. One of them was `svector(svector&&, Allocator const&)` taking over the other's allocation — every test that reached that constructor named an allocator that did not compare equal, so it always moved elements one at a time instead, including the case that is a compile time yes and is what every user of the default `std::allocator` gets. That is 1.3.0 code, in move and relocation logic, which is where this container's bugs have historically been. It works; nothing was checking. Now both storage modes are covered, and the stateful cases prove the takeover rather than assume it: no second allocation on the ledger, and the elements still at the address they started at.

Three of the nine are gone. The remaining six are all provably unreachable rather than merely untested, and are kept because they are cheap and they document invariants.

## Compatibility

C++17, single header, MIT, little endian. The inline namespace changes from `v1_3_0` to `v1_4_0`.

Share this resource


Discovered 2026-08-07 Source GitHub Archive 2026-08 →