make_uninitialized_space() raises size() and then returns raw storage for the caller to fill:
shift_right(p, data<D>() + s, p + count);
set_size<D>(s + count); // committed before anything is constructed
return p;
If the caller's construction throws partway, the container is left claiming elements that were never built — or, worse, that were built and then destroyed by the cleanup inside std::uninitialized_fill_n. The destructor then destroys them a second time.
auto v = ankerl::svector<ThrowOnCopy, 8>();
v.emplace_back("aaa...");
v.emplace_back("bbb...");
ThrowOnCopy::budget = 1; // the 2nd copy throws
v.insert(v.begin(), 3, ThrowOnCopy("ccc..."));
size=2 capacity=8
inserting 3 copies, the 2nd throws...
caught: copy
after: size=5
=================================================================
ERROR: AddressSanitizer: heap-use-after-free
#6 ThrowOnCopy::~ThrowOnCopy() f3.cpp:17
#7 std::_Destroy<ThrowOnCopy>(ThrowOnCopy*)
size() is 5, but slot 0 and 1 hold destroyed objects. Reading them is a use-after-free and the destructor double-frees.
Affects everything that goes through make_uninitialized_space():
insert(pos, count, value)
insert(pos, value) / insert(pos, T&&)
insert(pos, first, last) (forward iterators)
emplace(pos, ...)
std::vector gives the strong guarantee for a single-element insert and at least the basic guarantee for the rest; here even the basic guarantee is violated, since the container is left in a state that cannot be destroyed.
Suggestion
Commit the size only after the elements exist, the way emplace_back() already does:
// construct before updating the size, so a throwing constructor doesn't leave the
// vector claiming an element that was never built
auto& element = *new (static_cast<void*>(ptr)) T(std::forward<Args>(args)...);
set_size(s + 1);
That means make_uninitialized_space() has to hand back the gap without committing, and each caller commits once its fill succeeded — plus cleaning up the shifted tail on the way out if it wants more than the basic guarantee.
Reproduced with g++ 16, -O1 -fsanitize=address, libstdc++.
make_uninitialized_space()raisessize()and then returns raw storage for the caller to fill:If the caller's construction throws partway, the container is left claiming elements that were never built — or, worse, that were built and then destroyed by the cleanup inside
std::uninitialized_fill_n. The destructor then destroys them a second time.size()is 5, but slot 0 and 1 hold destroyed objects. Reading them is a use-after-free and the destructor double-frees.Affects everything that goes through
make_uninitialized_space():insert(pos, count, value)insert(pos, value)/insert(pos, T&&)insert(pos, first, last)(forward iterators)emplace(pos, ...)std::vectorgives the strong guarantee for a single-elementinsertand at least the basic guarantee for the rest; here even the basic guarantee is violated, since the container is left in a state that cannot be destroyed.Suggestion
Commit the size only after the elements exist, the way
emplace_back()already does:That means
make_uninitialized_space()has to hand back the gap without committing, and each caller commits once its fill succeeded — plus cleaning up the shifted tail on the way out if it wants more than the basic guarantee.Reproduced with g++ 16,
-O1 -fsanitize=address, libstdc++.