Skip to content

v1.4.0

Latest

Choose a tag to compare

@martinus martinus released this 05 Aug 05:07

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.