Skip to content

NiceAndPeter/bound

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

260 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bound

CI

Status: alpha. The library is under active development and the public API may change between versions. bound was 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.

Quick start

#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 == 100

Single header

The 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 needed

It 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.hpp and #include it; 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).

Feature highlights

  • Policy-driven assignmentclamp, wrap, sentinel, round_nearest, snap, plus on_clamp / on_wrap / on_overflow callbacks and a bnd::errc mode 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 — write a + 1_b, not a + 1 (a raw int/double has no grid). Bound-space dot / cross / lerp keep 2-D geometry inside the bounded world. See docs/arithmetic.md.
  • Conversions and casts — typed-error to<T>() / direct as<T>() (member and free forms), exact numerator() / denominator() read-out, conversion predicates (will_conversion_overflow, is_conversion_lossy), implicit operator imax() so a bound indexes arrays directly, and the clamp_cast / wrap_cast / clamp_round family. See docs/conversions.md.
  • Optimal storage & iteration — automatic raw-type selection (uint/int sizes, or an exact-fraction representation for non-dyadic grids), slim::optional with a sentinel encoding (no size overhead), bound_range for compile-time iteration, and STL/ranges integration. Plus predefined hardware-width aliases (bnd::byte, bnd::unorm16, bnd::q8_8, …) in bound/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-free constexpr), each bit-identical across platforms. The unqualified bnd::math::fn picks the build default (-DBOUND_MATH_FIXED=ON → cordic, -DBOUND_MATH_FLOAT=ON → float, else double); -DBND_MATH_NO_FP drops <cmath> for bare metal. Operands need only the snap bit (f64/f32 storage 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.

Documentation

Examples

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 u8u64 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_signed

Performance

Measured 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::store and from_value — storing an exact fraction is one gcd plus three integer multiplies; storing a double adds only its exact decomposition.
  • checked policy on accumulation pays a 4× penalty: the per-element domain check breaks autovectorisation. Use unsafe for tight inner loops where no-overflow is proven upfront, then convert back to a checked bound after the loop.
  • The math::* rows measure the call alone (the bench constructs inputs outside the timed blocks); f64/f32 operands are fp-backed, so input marshalling is free and the gap to std:: is the output grid-snap. math::fmod on commensurable integer grids beats std::fmod — it is a single integer remainder.
  • The two math::sin rows 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's std::sin). The float engine (math::flt::*, binary32) tracks the double engine closely on a double-capable host (~1.15× here) and is comparable to std::sinf; its real win is on single-precision-only FPUs (Cortex-M4F), where pairing it with f32 storage keeps the whole path in hardware float — no soft-double at the I/O boundary — rather than throughput on this box.

Build & Test

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 binaries

Regenerating the single header

The 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.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors