Skip to content

Fix resize() leaking when an element constructor throws - #77

Merged
martinus merged 1 commit into
mainfrom
fix-70-resize-leak
Jul 27, 2026
Merged

Fix resize() leaking when an element constructor throws#77
martinus merged 1 commit into
mainfrom
fix-70-resize-leak

Conversation

@martinus

Copy link
Copy Markdown
Owner

Fixes #70.

resize_after_reserve() constructed the new elements in a loop and only recorded the new size once the loop had finished. If element k threw, elements current_size..k were alive but the size never moved, so nothing ever destroyed them.

caught: ctor
done
Direct leak of 175 byte(s) in 5 object(s)

Exactly the 5 elements that had been constructed.

Fix

My first attempt wrapped the loop in a try/catch. The review pushed back from four directions and it was right: the loop should not exist. std::uninitialized_value_construct_n and std::uninitialized_fill_n both destroy what they built before rethrowing, which is precisely the missing behaviour — and it is already why insert() constructs through them instead of looping. fill_uninitialized_space's own doc comment says so.

if constexpr (sizeof...(Args) == 0) {
    std::uninitialized_value_construct_n(first_new, count - current_size);
} else {
    std::uninitialized_fill_n(first_new, count - current_size, args...);
}

So the fix deletes code rather than adding it, and resize_after_reserve stops being the one place in the file that hand-rolls what the rest gets from the standard algorithms.

One trap worth naming: two of the four reviewers suggested std::uninitialized_default_construct_n. That would have been a silent regression — it default-initializes, so resize(n) on an svector<int> would leave the new elements holding garbage instead of zeroing them the way T() does. value_construct is the one that matches. Verified directly:

svector after regrow: 0 0 0 ... (mismatches vs std::vector: 0)

Guarantee

Size and element values are unchanged on failure. Capacity may already have grown, because reserve() runs before the construction — std::vector::resize promises the strong guarantee including capacity, so this is the basic guarantee plus "size and values unchanged", not literally strong. That ordering predates this change.

Tests

Three cases in test/unit/resize.cpp sweeping start sizes 0-6 × target sizes up to 12 × first/last throw point, across resize(count), resize(count, value) and svector(count). Reverting to the loop leaks 450 objects under LSan.

Cost

Compared -O3 codegen before and after: .text identical at 9749 bytes, no .gcc_except_table in either, and the int fill still vectorizes to the same movups sequence.

Audit

Checked every other placement-new in the header. emplace_back_grow has its own guard, emplace_back constructs before committing the size, and the three single-element insert/emplace sites go through fill_uninitialized_space. This was the last multi-element construction loop without cleanup.

resize_after_reserve() built the new elements in a hand written loop and only
recorded the new size once the whole loop had finished. A constructor throwing
halfway left everything already built unreachable: the size still said
current_size, so nothing ever destroyed them.

Rather than wrapping the loop in a try/catch, the loop is gone. Both
std::uninitialized_value_construct_n and std::uninitialized_fill_n destroy what
they built before rethrowing, which is exactly the missing behaviour, and it is
already why insert() constructs through them instead of looping.

Value construction rather than default construction, so resize(n) on an
svector<int> keeps zeroing the new elements the way T() did.

Reachable through resize(count), resize(count, value), svector(count) and
svector(count, value). Reverting it leaks the 450 objects the new tests build.

Closes #70

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@martinus
martinus merged commit e842367 into main Jul 27, 2026
6 checks passed
@martinus
martinus deleted the fix-70-resize-leak branch July 27, 2026 19:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

resize() and svector(count) leak every element already built when a constructor throws

1 participant