Status: alpha. The library is under active development and the public API may change between versions.
boundwas developed with Claude Code, Anthropic's agentic coding tool.
A header-only C++23 library providing safe arithmetic on bounded rational
number grids. A grid is defined by a lower and upper (inclusive) bound, hence
the name, and a notch (step size); all three are exact fractions, and
(Upper − Lower) / Notch must be an unsigned integer. You write those
fractions with literals, notch<N, D>, and frac<N, D> — the exact-fraction
representation itself is an internal detail you never name.
Arithmetic operators (+, -, *, /) are type-safe by construction: the
result type's grid is computed at compile time to contain every possible
value. Policy only governs what happens when a value is assigned or
converted into a narrower type.
#include "bound/bound.hpp"
using namespace bnd;
// A percentage: integer values in [0, 100].
using pct = bound<{0, 100}>;
pct x = 42;
pct y = 58;
auto sum = x + y; // bound<{0, 200}> — no overflow possible
// Fractional grid: −1 .. 1 in 1/16 384 steps (Q1.14 audio sample).
using sample = bound<{{-1, 1}, notch<1, 16384>}, round_nearest>;
sample s = 0.5; // dyadic literal — exact
s.numerator(); // 1 (denominator() == 2): exact read-out
// Clamped percentage: saturates instead of throwing.
using safe_pct = bound<{0, 100}, clamp>;
safe_pct p = 150; // p == 100The whole library is also available as one self-contained header at
single_include/bound/bound.hpp. It inlines the
entire bound/ + slim/ tree, so it needs only the C++ standard library — there
are no bound/... or slim/... sub-includes left to resolve. Drop the one file
into a project, put single_include/ on the include path, and use it exactly as
the full tree:
#include "bound/bound.hpp" // single_include/ on the include path — nothing else neededIt is behaviourally identical to the multi-header form; the -DBOUND_MATH_FIXED
engine switch and the C++20 mode apply the same way, as ordinary compiler flags.
On Compiler Explorer, where there is no include tree to set up, the single header is the easy way in:
-
paste it into a second source pane named
bound/bound.hppand#includeit; or -
once the repo is on GitHub, pull it in with a single raw-URL include (Compiler Explorer resolves URL includes only for single-header libraries — which is exactly what this is):
#include <https://raw.githubusercontent.com/NiceAndPeter/bound/main/single_include/bound/bound.hpp>
The header is generated, not hand-edited — regenerate it after changing anything
under include/ with cmake --build build --target amalgamate (see
Build & Test).
- Policy-driven assignment —
clamp,wrap,sentinel,round_nearest,snap, pluson_clamp/on_wrap/on_overflowcallbacks and abnd::errcmode for throw-free error reporting. Representation flags (real,exact,direct,indexed) select how the raw value is stored. See docs/policies.md. - Type-safe widening arithmetic —
+ - * /widen the result grid at compile time; integer fast paths keep the common case at native speed. Scalars need a grid — writea + 1_b, nota + 1(a rawint/doublehas no grid). Bound-spacedot/cross/lerpkeep 2-D geometry inside the bounded world. See docs/arithmetic.md. - Conversions and casts — typed-error
to<T>()/ directas<T>()(member and free forms), exactnumerator()/denominator()read-out, conversion predicates (will_conversion_overflow,is_conversion_lossy), implicitoperator imax()so a bound indexes arrays directly, and theclamp_cast/wrap_cast/clamp_roundfamily. See docs/conversions.md. - Optimal storage & iteration — automatic raw-type selection (uint/int
sizes, or an exact-fraction representation for non-dyadic grids),
slim::optionalwith a sentinel encoding (no size overhead),bound_rangefor compile-time iteration, and STL/ranges integration. Plus predefined hardware-width aliases (bnd::byte,bnd::unorm16,bnd::q8_8, …) inbound/formats.hpp. See docs/storage.md. - Reproducible math, three engines — a
<cmath>-shaped function set over bounds (sin/cos/tan,asin/acos/atan/atan2,sinh/cosh/tanh,exp/log/log2/log10/pow,sqrt/cbrt/hypot). One API; three engines callable side-by-side by namespace (dbl::binary64,flt::binary32,cordic::integer/FPU-freeconstexpr), each bit-identical across platforms. The unqualifiedbnd::math::fnpicks the build default (-DBOUND_MATH_FIXED=ON→ cordic,-DBOUND_MATH_FLOAT=ON→ float, else double);-DBND_MATH_NO_FPdrops<cmath>for bare metal. Operands need only thesnapbit (f64/f32storage is an optional fast path); angles are radians; output grids auto-deduce. See docs/math.md. - Library internals — grid invariants, storage decision tree, Q-format fast path, policy cascade. See docs/internals.md.
- Tutorial — the mental model
- Policies, callbacks & error handling
- Arithmetic, rounding & compound assignment
- Conversions, casts & predicates
- Storage, iteration & STL integration
bnd::math— constexpr, bit-exact math- Determinism & reproducibility
- Bound for fixed-point users
- Freestanding & bare-metal
- Reading a
bound<>in a compiler error - Internals (architecture / design notes)
- Roadmap — features gated on future C++ standards
- Resources — prior art & talks
The examples/ directory contains ~30 self-contained programs. A curated
selection:
| Example | Feature |
|---|---|
percentage.cpp |
Clamped percentage with += and with_clamp() |
clock.cpp |
Cascading wrap with carry (seconds → minutes → hours) |
audio_sample.cpp |
Signed Q1.14 audio samples with mixing and clamp |
money.cpp |
Cents-precision currency arithmetic via fractional notch |
pid_controller.cpp |
Fixed-point PID loop with add_all and clamp | round_nearest actuator |
audio_mixer.cpp |
4-channel Q1.14 mix with with(on_clamp, on_overflow) peak metering |
sensor_fusion.cpp |
Weighted average across sensors with disparate fixed-point ranges |
torus_map.cpp |
2-D sub-pixel position with wrap on both axes and edge-crossing events |
algorithms.cpp |
STL and ranges algorithms (sort, find, transform, accumulate, …) |
formats.cpp |
Predefined hardware-width types (byte / sword / unorm16 / q8_8) and interop |
storage_flags.cpp |
Pin the raw type with the u8…u64 width flags (value vs indexed); compile-time fit check |
Build and run any example:
cmake -B build && cmake --build build
./build/example_clock
./build/example_signedMeasured at -O3 -DNDEBUG on x86-64 via tests/bench.cpp (5M iterations per
scenario, native baseline paired with each bound case). Lower is better.
| Workload | bound | native | ratio |
|---|---|---|---|
bound<{0,200}> ±/×/÷ (integer raw, unsafe) |
13 ns | 13 ns | 1.0× |
bound<{{0,255},1/256}> construct (Q8.8) |
13 ns | 14 ns | 0.97× |
bound<{{0,65535},1/65536}> construct (Q16.16) |
14 ns | 14 ns | 0.97× |
accumulate(bound, unsafe) 1000 elts |
64 ns | 64 ns | 1.0× (vectorized) |
accumulate(bound, checked) 1000 elts |
274 ns | 64 ns | 4.3× (scalar) |
bnd::sum<checked> 1000 elts (one deferred check) |
105 ns | 64 ns | 1.6× (vectorized) |
transform(b += 1) 10k uint8-width elts (unsafe) |
1.02 µs | 1.02 µs | 1.0× (SIMD) |
| Q-format store from exact fraction | ~6 ns | n/a | n/a |
Q-format store from double |
~46 ns | n/a | n/a |
math::sin (f64 operand, double engine) |
62 ns | 107 ns (std::sin) |
0.58× |
math::flt::sin (f32 operand, float engine) |
72 ns | 77 ns (std::sinf) |
0.94× |
math::fmod (integer grids) |
19 ns | 25 ns (std::fmod) |
0.76× |
Notes:
- Integer-raw bounds (the common case:
bound<{0,N}>,bound<{a,b}>with notch 1) are at native parity for arithmetic and assignment. Unchecked compound ops run at the raw type's width, so byte-wide loops vectorize at native lane count. (Mind the sentinel slot:bound<{0,255}>stores in uint16 — see docs/storage.md.) - Q-format grids (integer Lower, unit-numerator Notch) take integer fast
paths in
assignment::storeandfrom_value— storing an exact fraction is onegcdplus three integer multiplies; storing adoubleadds only its exact decomposition. checkedpolicy on accumulation pays a 4× penalty: the per-element domain check breaks autovectorisation. Useunsafefor tight inner loops where no-overflow is proven upfront, then convert back to acheckedbound after the loop.- The
math::*rows measure the call alone (the bench constructs inputs outside the timed blocks);f64/f32operands are fp-backed, so input marshalling is free and the gap tostd::is the output grid-snap.math::fmodon commensurable integer grids beatsstd::fmod— it is a single integer remainder. - The two
math::sinrows are a fresh same-run measurement (so they are directly comparable to each other; absolute ns is machine-dependent — here the own-polynomial double engine even edges out this libm'sstd::sin). The float engine (math::flt::*, binary32) tracks the double engine closely on a double-capable host (~1.15× here) and is comparable tostd::sinf; its real win is on single-precision-only FPUs (Cortex-M4F), where pairing it withf32storage keeps the whole path in hardwarefloat— no soft-doubleat the I/O boundary — rather than throughput on this box.
Requires CMake 3.24+ and a C++23 compiler (GCC 13+, Clang 16+, MSVC 19.36+) for the full feature set.
The library also builds against C++20 on GCC 12: configure with
-DBOUND_CXX20=ON. In that mode the error channel uses the bundled
slim::expected backport instead of <expected>, and the std::format
integration is feature-gated off (to_string() / operator<< remain available)
— everything else is identical.
cmake -B build
cmake --build build
# C++20 / GCC 12 build:
cmake -B build20 -DBOUND_CXX20=ON -DCMAKE_CXX_COMPILER=g++-12
cmake --build build20
# Integer/CORDIC math engine (constexpr, FPU-free, unconditionally
# bit-identical — see docs/math.md):
cmake -B build-fixed -DBOUND_CXX20=ON -DBOUND_MATH_FIXED=ON
cmake --build build-fixed
ctest --test-dir build --output-on-failure # runs unit + algo suites
./build/bound_tests # unit tests directly
./build/bench # performance benchmarks (native vs bound)
./build/example_algorithms # one of the example binariesThe committed single header is generated from include/ by a
pure-CMake amalgamator (cmake/amalgamate.cmake — no Python or other tooling).
After editing any header under include/, regenerate and commit it:
cmake --build build --target amalgamate # rewrites single_include/bound/bound.hpp
ctest --test-dir build -L tooling # amalgamate_up_to_date: fails if it drifted
cmake --build build --target single_header_smoke # compiles a TU seeing ONLY single_include/ctest runs amalgamate_up_to_date as part of the normal suite, so a stale
single header fails the build until it is regenerated.