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 answerPTRDIFF_MAXfor everyT, which claimed a size nosvectorcould reach — allocation has always refused anything whose bytes passPTRDIFF_MAX, so the real ceiling wasPTRDIFF_MAX / sizeof(T)all along. That is now what it says, and it is whatstd::vectorsays. Forsvector<int>it goes from 9223372036854775807 to 2305843009213693951. Stillstatic, for the reason the comment there gives. -
Asking for more than that throws
std::length_error, notstd::bad_alloc. These are different questions andstd::vectorspells them differently: a size that cannot exist is alength_error; a size that can but that the allocator will not give is abad_alloc. Soreserve(max_size())is still abad_alloc— that size is legal and the allocation is what fails — while one past it is now alength_error.This is the one thing to check before upgrading. If you catch
std::bad_allocaround areserve,resizeorinsertthat might be handed an unreasonable count, that handler no longer runs;std::length_errorderives fromstd::logic_error, not fromstd::bad_alloc. Catchingstd::exceptionis unaffected.
Added
-
The C++23 range members:
assign_range,append_range,insert_range, and thestd::from_rangeconstructor. The README claimedsvectorimplements all ofstd::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 ofviews::filteroverviews::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_rangesrather than on the language version, because what they need isstd::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.