Skip to content

Add resize_and_overwrite (reworked #53) - #59

Merged
martinus merged 2 commits into
mainfrom
pr/53-resize-and-overwrite
Jul 27, 2026
Merged

Add resize_and_overwrite (reworked #53)#59
martinus merged 2 commits into
mainfrom
pr/53-resize-and-overwrite

Conversation

@martinus

Copy link
Copy Markdown
Owner

Reworks #53 by @Andersama. Their original commit is preserved as the first commit here, so merging this closes #53 as merged.

The idea is good and matches where the discussion in #49 landed. The implementation had three defects, all verified by running it.

1. op received the wrong number

std::string::resize_and_overwrite(count, op) calls op(p, count) — the requested size. The original called op(p, current_size) — the old size:

resize_and_overwrite(8) on a size-3 vector:
  libstdc++ std::string  ->  callback gets n=8
  #53 as submitted       ->  callback gets n=3

Same signature, different meaning, no diagnostic. A callback written against the cppreference example the PR cites as its guide would think it has 3 writable slots when it has 8. This is the defect that most needed fixing, since it is silent.

2. Double destruction if op throws while shrinking

The tail was destroyed via destroy_n but the stored size was left untouched while op ran, so ~svector() destroyed those elements again:

ERROR: AddressSanitizer: attempting double-free
  #9  svector<std::string, 2>::destroy() svector.h:525
  #10 svector<std::string, 2>::~svector() svector.h:600

3. Full element leak if op throws while growing

Elements were moved into the new storage and set_indirect was called, but the size stayed 0 until after op returned, so an exception lost all of them: Direct leak of 190 byte(s) in 5 object(s).

The rework

reserve() first, commit the shrunken size before calling op, commit op's result after. At every point where op can throw, the stored size describes exactly the live elements. This also makes it a lot shorter, since reserve() and the non-template set_size() already handle the direct/indirect split.

Also dropped constexprsvector uses reinterpret_cast, memcpy and placement new, so it can never be constant-evaluated, and nothing else in the API claims it.

Tests

New test/unit/resize_and_overwrite.cpp, 11 cases: the op(p, count) contract, growth within and past inline capacity, shrinking, returning less than count, resize to 0, non-trivial element types, that no value-initialization happens, and both throwing-op paths. Counter verifies every element is destroyed exactly once.

5 of the 11 fail against the original implementation (n == 0 gets 10, n == 50 gets 2, plus ERROR at ~Counter(): got 2 objects still alive! / SIGABRT), so they are real regression guards rather than decoration.

Full suite: Ok: 2, Fail: 0. Lint clean.

Note on scope

resize_and_overwrite is string-only in C++23, so this is a non-standard extension to a vector either way — that part is your call. If it goes in, it should match the standard signature exactly, which it now does.

Depends on #56 for CI; this branch is based on the pre-fix main, so its checks will be red until that merges.

🤖 Generated with Claude Code

This was referenced Jul 27, 2026
Andersama and others added 2 commits July 27, 2026 17:42
Following feedback from #49 I've written `resize_and_overwrite` using https://en.cppreference.com/w/cpp/string/basic_string/resize_and_overwrite as a guide.
Reworked the implementation. Three problems with the previous version:

1. op was called as op(p, current_size) instead of op(p, count). The
   standard passes the requested count, and that is the whole point of
   the API: the callback needs to know how much writable space it has.
   Passing the old size silently breaks any callback written against
   cppreference, with an identical signature. For a size 3 vector
   resized to 8, libstdc++ passes n=8 while this passed n=3.

2. Shrinking destroyed the tail via destroy_n but left the stored size
   untouched while op ran. If op threw, ~svector() destroyed those
   elements a second time (confirmed as a double free under ASan).

3. Growing moved the elements into the new storage and installed it,
   but left the size at 0 until after op returned. If op threw, every
   existing element leaked.

The rewrite reserves first, commits the shrunken size before calling
op, and only then commits op's result, so at every point where op can
throw, the stored size describes exactly the live elements. That also
makes it much shorter, since reserve() and set_size() already handle
the direct/indirect split.

Dropped constexpr: svector uses reinterpret_cast, memcpy and placement
new, so it can never be constant evaluated, and none of the rest of the
API claims constexpr.

Added test/unit/resize_and_overwrite.cpp with 11 cases: the op(p, count)
contract, growing inside and past the inline capacity, shrinking,
returning less than count, resizing to 0, non trivial element types,
that no value initialization happens, and both throwing op paths. The
Counter helper verifies every element is destroyed exactly once. Five of
these fail against the previous implementation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@martinus
martinus force-pushed the pr/53-resize-and-overwrite branch from a88d93a to 69cb2f9 Compare July 27, 2026 15:42
@martinus
martinus merged commit 6b2c479 into main Jul 27, 2026
8 checks passed
@martinus
martinus deleted the pr/53-resize-and-overwrite branch July 27, 2026 15:46
@Andersama

Copy link
Copy Markdown
Contributor

Apologies about the exception safety, I tend to work with POD-like types where destruction is trivial and exceptions don't tend to get thrown.

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.

2 participants