Skip to content

Consistent debug/release precondition checks + index_range hardening (#29)#33

Merged
jkalias merged 3 commits into
mainfrom
claude/open-issues-list-z2d0z7
Jun 13, 2026
Merged

Consistent debug/release precondition checks + index_range hardening (#29)#33
jkalias merged 3 commits into
mainfrom
claude/open-issues-list-z2d0z7

Conversation

@jkalias

@jkalias jkalias commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Summary

Addresses #29. The unifying theme is making the library behave consistently between debug and release builds, plus the smaller index_range hardening from the same issue.

1. Consistent precondition checks across debug and release

Previously every safety check was an assert, which the standard disables when NDEBUG is defined. So checks fired (aborting) in debug but vanished in release, turning operator[] and replace_range_at into silent undefined behavior / buffer overflows there.

Introduced FCPP_PRECONDITION in compatibility.h — an always-on check that evaluates its condition and calls std::abort() on failure in every build, preserving the library's existing fatal error model:

#ifdef FCPP_NO_PRECONDITION_CHECKS
#define FCPP_PRECONDITION(condition) ((void)0)
#else
#define FCPP_PRECONDITION(condition) do { if (!(condition)) { std::abort(); } } while (false)
#endif

All assert / assert(false); std::abort(); precondition checks in vector, set, and map were converted to it (bounds checks, replace_range_at bounds, zip size-mismatch, map's const operator[] missing-key). Define FCPP_NO_PRECONDITION_CHECKS to compile the checks out for hot paths whose inputs are already validated.

Before (release) After (release)
operator[] out of bounds silent UB abort (same as debug)
replace_range_at oversized silent overflow abort (same as debug)
zip size mismatch abort abort

replace_range_at's bounds expression was also rewritten to be overflow-safe (vec_size <= size() && index <= size() - vec_size).

2. index_range hardening

  • index_range::invalid is now const, so the shared sentinel can't be mutated by callers.
  • remove_range / removing_range compare size() against range.end directly instead of range.end + 1, removing a potential signed-overflow at INT_MAX and the size_t/int mismatch. Behavior is unchanged for all valid ranges.

3. Docs

Added an "Error handling" section to the README documenting the always-on checks and the FCPP_NO_PRECONDITION_CHECKS opt-out.

Tests

  • Added MapTest.AccessConstOperatorMissingKeyDeath (the const operator[] missing-key path previously had no coverage).
  • All existing EXPECT_DEATH tests now pass under release as well as debug.

Verification

Built and ran the full suite under Debug and Release × C++11 and C++17 — all green (301 tests in C++17). The FCPP_NO_PRECONDITION_CHECKS opt-out path also compiles cleanly.

Note on scope

This intentionally keeps the library's fatal-abort error model (the maintainer's existing, death-test-locked choice) rather than switching to exceptions — it just makes that model consistent across build types. Switching to exceptions would be a separate, larger decision.

Closes #29.

https://claude.ai/code/session_011y3XcVg3UCcgi5JMEo2tso


Generated by Claude Code

claude added 3 commits June 13, 2026 18:40
…ecks (#29)

Make index_range::invalid a const static so the shared sentinel cannot be
mutated by callers.

In vector::remove_range / removing_range, compare the vector size against
range.end directly instead of `range.end + 1`. A valid range guarantees
range.end >= 0, so casting to size_t is safe; this removes the potential
signed-integer overflow at INT_MAX and the signed/unsigned comparison
between size() and an int. Behavior is unchanged for all valid ranges.

Note: the assert/abort-on-misuse contract (e.g. replace_range_at bounds,
zip size mismatches) is intentionally left as-is; it is the library's
documented, death-tested error model and changing it to exceptions is a
separate decision.
The library guarded out-of-bounds and size-mismatch operations with assert,
which the standard disables when NDEBUG is defined. As a result the checks
fired (aborting) in debug but vanished in release, turning operator[] and
replace_range_at into silent undefined behavior / buffer overflows there.

Introduce FCPP_PRECONDITION in compatibility.h: an always-on check that
evaluates its condition and calls std::abort() on failure in every build,
preserving the library's existing fatal error model. Replace the assert /
"assert(false); std::abort();" precondition checks in vector, set and map
with it. Define FCPP_NO_PRECONDITION_CHECKS to compile the checks out for
hot paths whose inputs are already validated.

Debug and release now behave identically; all existing EXPECT_DEATH tests
pass under both build types. Added a death test for map's const operator[]
with a missing key, which previously had no coverage.
@jkalias jkalias merged commit a60aef7 into main Jun 13, 2026
12 checks passed
@jkalias jkalias deleted the claude/open-issues-list-z2d0z7 branch June 13, 2026 19:08
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.

Memory safety / robustness: replace_range_at buffer overflow, index_range int overflow, abort() on misuse, mutable global

2 participants