Skip to content

Add erase() to VariableLengthArray (#79) - #242

Merged
thirtytwobits merged 3 commits into
mainfrom
claude/issue-79-vla-erase
Oct 19, 2026
Merged

thirtytwobits merged 3 commits into
mainfrom
claude/issue-79-vla-erase

Conversation

@thirtytwobits

Copy link
Copy Markdown
Member

Adds erase() to cetl::VariableLengthArray, matching std::vector::erase:

iterator erase(const_iterator pos);
iterator erase(const_iterator first, const_iterator last);

Both the general and the packed-bool versions:

  • move later elements toward the front, then destroy the vacated tail (for bool, they shorten the bit count)
  • keep capacity and max_size(); iterators before the erased position stay valid
  • return an iterator to the element that followed the removed range, which is end() if the range reached it
  • treat an empty range as a no-op, including on an array that has never allocated

Exceptions. The general erase is noexcept when value_type's move assignment and destructor are. If a move assignment throws, the size and every object's lifetime are unchanged, but values may have been moved from. Packed-bool erase can't fail and is noexcept.

bool iterators. std::vector<bool>::erase takes const_iterator, so bits.erase(bits.begin()) needs iterator to convert to const_iterator. The bool specialization's proxy iterator had no such conversion, so this PR adds one, as standard containers provide. A const_iterator still doesn't convert to iterator.

Debug assertions. Four new checks:

  • CDE_vla_009 (general) and CDE_vla_011 (bool): the position is not dereferenceable, e.g. end()
  • CDE_vla_010 (general) and CDE_vla_012 (bool): the range is outside the array, reversed, or, for bool, from another array

The bool size update that range assignment already used moves into a small set_size_bits() helper shared with erase.

Tests

  • New test_variable_length_array_erase.cpp, typed over int and bool and checked against std::vector. It covers:
    • every position, and every [first, last) of a 17-element array, across packed-byte boundaries
    • the returned iterator and unchanged capacity, plus push_back afterward to check the bit bookkeeping
    • mutable and const iterator arguments, and that the conversion only goes one way
    • empty ranges (including on a never-allocated array), erasing everything, erase-while-iterating, and iterator validity
    • std::string and std::unique_ptr elements, and the noexcept specification
  • test_variable_length_array_exception_safety.cpp: erase destroys only the vacated tail, and a throwing move assignment keeps the size and every lifetime.
  • test_variable_length_array_debug_asserts.cpp: death tests for the four new assertions.
  • Fuzzer: the pop_back operation now also chooses erase(pos) or erase(first, last) from its spare opcode bits, so the input format is unchanged. It checks the returned position and capacity, and compares the result against std::vector after every operation.

Validation

  • GCC 13, C++14: all 13 VLA test suites pass in Debug and DebugEP (-fno-exceptions), including the death tests
  • GCC 7.5 (toolshed ts24.4.3): the erase, exception-safety and death-test suites pass with and without exceptions, with -Werror -Wconversion -Wsign-conversion
  • GCC 13 and Clang, C++14/17/20, with and without exceptions: the erase, exception-safety, debug-assert, assign and bool suites compile cleanly with -Wall -Wextra -Werror -Wconversion -Wsign-conversion -pedantic
  • Clang, C++20, ASan + UBSan, debug asserts on: the erase, exception-safety, assign and bool suites pass
  • libFuzzer, default and embedded profiles: ctest passes, plus 60 s runs of about 660k inputs each with no findings
  • clang-format 19.1.7 from toolshed:ts24.4.3 reports no changes for the header, the new suite or the fuzzer; git diff --check is clean. The new death tests follow the existing style of test_variable_length_array_debug_asserts.cpp, which isn't clang-formatted.

Closes #79.

🤖 Generated with Claude Code

Add `erase(const_iterator pos)` and `erase(const_iterator first,
const_iterator last)`, matching std::vector::erase, for both ordinary
elements and packed bool. Later elements move toward the front, and the
vacated tail is destroyed (or, for bool, the bit count is reduced).
Capacity and max_size() are unchanged, iterators before the erased
position stay valid, and the returned iterator points at the element
that followed the removed range. An empty range has no effect, including
on an array that has never allocated.

The general erase is noexcept when value_type's move assignment and
destructor are. If a move assignment throws, the size and every
object's lifetime are unchanged, although values may have been moved
from. Packed-bool erase cannot fail and is noexcept.

std::vector<bool>::erase takes const_iterator, so the bool
specialization's mutable iterator now converts implicitly to its
const_iterator, as a standard container's iterator does. The bool
size-update arithmetic shared with range assignment moves into a
set_size_bits() helper.

New debug assertions reject a non-dereferenceable position
(CDE_vla_009, CDE_vla_011 for bool) and a range outside the array
(CDE_vla_010, CDE_vla_012 for bool).

Tests: a new test_variable_length_array_erase suite (int and bool,
checked against std::vector) covers every position and every range
across packed-byte boundaries, returned iterators, capacity, mutable and
const iterators, empty ranges, erase-while-iterating, iterator validity,
std::string and move-only elements, and noexcept. The exception-safety
suite checks object lifetimes on success and a throwing move assignment,
the debug-assert suite has death tests for the four new assertions, and
the fuzzer's pop_back operation now also chooses erase(pos) or
erase(first, last) from its spare opcode bits, without changing the
input format.

Closes #79.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thirtytwobits

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T16:33:49.547641Z 2892ef9 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1af2700e0b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread include/cetl/variable_length_array.hpp Outdated
EraseEverythingKeepsCapacityAndMaxSize compared `actual.erase(...)` with
`actual.end()` inside one EXPECT_EQ. The order in which those arguments
are evaluated is unspecified, and GCC's 32-bit C++20 build evaluated
end() first, so it compared against the end from before the erase.
Store each erase result before comparing it, here and in the other
tests that combined the two.

Also return end() straight away when the array has no storage, instead
of computing `data_ + (first - cbegin())` from null pointers. C++
defines that arithmetic, but end() already avoids it, and the early
return keeps erase the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thirtytwobits

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f7c0b84af1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread include/cetl/variable_length_array.hpp
Comment thread cetlvast/suites/fuzz/fuzz_variable_length_array.cpp
The bool specialization's iterator converts to const_iterator, but its
comparison and difference operators were members taking the same
iterator type. Only a right-hand iterator could convert, so
`bits.begin() != bits.cend()` and `bits.end() - bits.cbegin()` did not
compile, although they work for std::vector. Make ==, !=, <, >, <=, >=
and iterator difference hidden friends, so either operand can convert. A
new typed test covers every mixed combination in both orders.

In the fuzzer's erase path, erase an empty array as
`erase(cbegin(), cend())` and check the result with `== end()`, so the
harness does no arithmetic on a possibly null data pointer. This matches
how it already avoids subtracting null pointers elsewhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thirtytwobits

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 2892ef904d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@thirtytwobits
thirtytwobits merged commit 549ad98 into main Oct 19, 2026
29 checks passed
@thirtytwobits
thirtytwobits deleted the claude/issue-79-vla-erase branch October 19, 2026 16:42
thirtytwobits added a commit that referenced this pull request Oct 19, 2026
…ed ts26.4.5 (#244)

Adds reverse iterators to `cetl::VariableLengthArray`, matching
[`std::vector::rbegin`](https://en.cppreference.com/w/cpp/container/vector/rbegin)
and [`rend`](https://en.cppreference.com/w/cpp/container/vector/rend):

```cpp
using reverse_iterator       = std::reverse_iterator<iterator>;
using const_reverse_iterator = std::reverse_iterator<const_iterator>;

reverse_iterator       rbegin() noexcept;    const_reverse_iterator rbegin() const noexcept;
reverse_iterator       rend() noexcept;      const_reverse_iterator rend() const noexcept;
const_reverse_iterator crbegin() const noexcept;
const_reverse_iterator crend() const noexcept;
```

Both the general and the packed-`bool` versions get them. For `bool`,
`std::reverse_iterator` wraps the existing proxy iterators: writing
through them works, and since #242 made `iterator` convert to
`const_iterator`, a `reverse_iterator` converts to a
`const_reverse_iterator` and the two compare and subtract in either
order. Like the forward iterators, reverse iterators are invalidated by
`reserve()` and `shrink_to_fit()`. An array that has never allocated has
an empty reverse range.

## Tests
New `test_variable_length_array_reverse_iterators.cpp`, typed over `int`
and `bool` and checked against `std::vector`:
- member types, return types of every accessor (mutable and const), and
`noexcept`
- reverse traversal at sizes 0, 1, 7, 8, 9 and 17 (packed-byte
boundaries for `bool`), `distance`, `base()`, and random access
- the empty range of a never-allocated array
- writing through the adaptor (`*rbegin() = …`, `std::fill`) and
`std::reverse_copy` as an oracle
- mixed `reverse_iterator` and `const_reverse_iterator`: conversion,
`==`, `!=`, `<`, `>` and subtraction in both orders
- searching from the back with `std::find`
- `std::string` elements, including `operator->` through the adaptor

I left out a "`const_reverse_iterator` doesn't convert to
`reverse_iterator`" check. Before C++20, libstdc++'s
`std::reverse_iterator` converting constructor is unconstrained (the
constraint came with LWG 3435), so the trait reports `true` there even
for `std::vector`. It would test the standard library rather than VLA.

## Validation
- GCC 13, C++14: all 15 VLA test suites pass in Debug and DebugEP
(`-fno-exceptions`)
- GCC 13, C++20, and Clang C++20 with ASan + UBSan: the new suite passes
- GCC 13 and Clang, C++14/17/20/23, with and without exceptions: the
new, compat and bool suites compile cleanly with `-Wall -Wextra -Werror
-Wconversion -Wsign-conversion -pedantic`
- toolshed `ts24.4.3`: the new suite compiles cleanly with GCC 7
(C++14), GCC 13 (C++20) and Clang 19 (C++20)
- `clang-format` 19.1.7 from `toolshed:ts24.4.3` reports no changes;
`git diff --check` is clean

## Bit proxy swap (from review)
`VLA<bool>`'s proxy `reference` now has a hidden-friend `swap(reference,
reference)`, and the class has the static `swap(reference, reference)`
that `std::vector<bool>` provides. Without them, `std::reverse`,
`std::rotate`, `std::sort` and other algorithms that swap through
`std::iter_swap` did not compile on bits, with forward iterators as well
as the new reverse ones.

## CI: toolshed ts26.4.5
CI moves from `toolshed:ts24.4.3` to `ts26.4.5`, the latest release
(Ubuntu 26.04).

| Job | Image | Matrix |
| --- | --- | --- |
| `cache-warmup` | `ts26.4.5` | — |
| `verification-amd64` | `ts26.4.5` | `gcc-native-32`, `clang-native` ×
C++14/17/20/23 |
| `verification-arm64` | `ts26.4.5` | `gcc-native`, `clang-native` ×
C++14/17/20 |
| `verification-legacy-arm64` (new) | `ts24.4.3` | `gcc7-native` × C++14
|
| `docs` | `ts26.4.5` | — |

`ts26.4.x` no longer ships `g++-7`, so GCC 7 moves to its own job on the
last image that has it. The new image upgrades GCC 13.3 to 15.2, Clang
19.1 to 22.1, clang-format 19.1 to 22.1, and CMake 3.31 to 4.2. That
needed a few fixes, in a separate commit:
- **clang-format:** every build runs the header format check.
clang-format 22 lays out four declarations in `optional.hpp`,
`variant.hpp` and `rtti.hpp` differently from 19, so they are wrapped in
`clang-format off/on`. Every header now has zero violations under both
19 (the GCC 7 job) and 22.
- **`type_traits_ext.hpp`:** `detail::copy_list_initialize` declared a
by-value `volatile` parameter for `volatile` targets. C++20 deprecates
that, and Clang 22 errors on it under `-Werror`, which would also hit
users. It now takes `std::remove_cv_t<To>`.
- **`example_07`:** dropped a `virtual` destructor in a `final` class
(`-Wunnecessary-virtual-specifier`, new in Clang 22).
- **`test_variable_length_array_bool.cpp`:** the `std::vector<bool>`
oracle used an `unsigned char` allocator. The standard requires `bool`,
and GCC 15 enforces it from C++20.

Checked in `ts26.4.3`, which has the same toolchains as `ts26.4.5`;
later releases only added linters and Bazel. Results: the `gcc-native`
and `clang-native` preset workflows at C++14/17/20/23 (232 test runs
each), the `docs` build, and the online cache warmup. I couldn't run
`gcc-native-32` locally, so this PR's CI is its first run on the new
image.

Closes #65.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
thirtytwobits added a commit that referenced this pull request Oct 19, 2026
The insert tests and the fuzzer computed `x.insert(...) - x.begin()`
and `x.insert(...) == x.begin() + n` in one expression. The order in
which an expression's operands are evaluated is unspecified, and
insert() may reallocate. GCC 15's 32-bit C++17 build evaluated
std::vector's begin() first, then subtracted iterators into two
different buffers, so verification-amd64 (gcc-native-32, cpp-17)
failed AllOverloadsMatchVector against a garbage oracle. The same
mistake was fixed in the erase tests in #242.

Keep each returned iterator in a variable and compare it in a separate
statement, in the tests and in the fuzzer. No library change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Add erase() to VariableLengthArray

1 participant