Skip to content

Fix erase() of an empty range blanking the tail - #72

Merged
martinus merged 1 commit into
mainfrom
fix-66-erase-empty-range
Jul 27, 2026
Merged

Fix erase() of an empty range blanking the tail#72
martinus merged 1 commit into
mainfrom
fix-66-erase-empty-range

Conversation

@martinus

Copy link
Copy Markdown
Owner

Fixes #66. Same class of bug as #65, different function.

v.erase(it, it) blanked every element from it onwards:

erase(it, it)
  svector       [<EMPTY!> <EMPTY!> <EMPTY!> ]
  std::vector   [aaaa... bbbb... cccc... ]

erase_checked_end ran std::move(erase_end, container_end, erase_begin) unconditionally. With first == last the destination equals the source begin, which std::move does not allow, and every element self-move-assigns.

This was the last one

Grepped the whole header for the assigning algorithms. There are exactly two raw call sites:

line call status
492 std::move(...) in erase_checked_end fixed here
581 std::move_backward(...) in shift_right fixed in #65

Every other data movement goes through std::uninitialized_move / std::uninitialized_copy / std::uninitialized_fill_n, which construct into raw memory and so cannot self-assign. The remaining std::move hits are the single-object cast, not the algorithm. So this bug class is now fully covered.

Depth

The guard belongs in erase_checked_end, not the public erase(): to is clamped against container_end first, so a range can be non-empty on entry and still clamp to empty. Checking first == last in the caller would miss that case, and re-deriving the clamp there would duplicate the logic.

It reuses num_erased (hoisted above the guard) instead of spelling "nothing to erase" a second way as a pointer comparison.

Tests

One case in test/unit/erase.cpp, covering begin/middle/end positions in both storage modes. Fails without the guard.

make_long_strings() moved into test/app/VecTester.h#65's test grew the same "strings long enough to defeat SSO" scaffolding in insert.cpp, and this would have been the second copy. Both now call the helper.

Verified with gcc and clang under -fsanitize=address,undefined.

erase_checked_end ran std::move(erase_end, container_end, erase_begin)
unconditionally. When first == last the destination equals the source begin,
which std::move does not allow, and it self-move-assigns every element from
there to the end. With std::string they all come back empty.

Same class of bug as #65, and grepping the header for the assigning algorithms
shows these were the only two: every other data movement goes through
std::uninitialized_move or std::uninitialized_copy, which construct into raw
memory and cannot self-assign.

The guard reuses num_erased, hoisted above it, rather than comparing the
pointers a second way.

make_long_strings() moves to VecTester.h, since #65's test grew the same
"strings long enough to defeat SSO" helper in insert.cpp.

Closes #66

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@martinus
martinus merged commit 1e26665 into main Jul 27, 2026
6 checks passed
@martinus
martinus deleted the fix-66-erase-empty-range branch July 27, 2026 19:02
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.

erase(it, it) silently blanks every element from it onwards

1 participant