This is the top-down tour: what bound is, the three ideas it rests on, and
how a value flows through a program. For per-topic depth see the other docs;
for the why behind the design see internals.md.
A bound is a number that carries its own grid — a lower bound, an upper
bound, and a step size — all as exact fractions, checked at compile time. The
type is the contract; the runtime value can never leave it.
using pct = bound<{0, 100}>; // integers 0..100
using sample = bound<{{-1, 1}, notch<1, 16384>}>; // -1..1 in 1/16384 stepsA grid is { {Lower, Upper}, Notch }. Three exact fractions, one invariant:
(Upper − Lower) / Notch is a whole number — there is an integer count of
notches across the interval. You write the fractions with integer/dyadic
literals, notch<N, D>, and frac<N, D>; the underlying exact-fraction type
is internal and you never name it.
Because the grid is part of the type, two differently-shaped bounds are different types — the compiler tracks range and precision for you.
pct a = 70, b = 58;
auto s = a + b; // type is bound<{0, 200}> — computed at compile time+ - * / compute the result grid at compile time to contain every value
the operation could produce. So a + b cannot overflow: its type is already
wide enough. The result is a new bound type; nothing is checked at runtime on
the happy path, and integer-aligned grids hit native-integer fast paths.
Scalars need a grid too — write a + 1_b, not a + 1 (a raw int has no
grid). Bound-space dot / cross / lerp follow the same widening rule.
See arithmetic.md.
Widening is free; the interesting decisions happen when a value is assigned or converted into a narrower grid. That is the only place a value can fail to fit, and a policy decides what happens:
bound<{0, 100}, clamp> p = 150; // p == 100 (saturate)
bound<{0, 359}, wrap> deg = 370; // deg == 10 (modular)
bound<{0, 100}> q = 150; // throws (default: checked)Policies: clamp, wrap, sentinel, the round_* family, plus per-operation
callbacks (on_clamp, on_wrap, …) and a throw-free bnd::errc mode.
See policies.md.
using mix = bound<{{-4, 4}, notch<1, 16384>}, round_nearest | clamp>;
sample a = read_input(); // 1. enter the bounded world once
auto m = a * gain + offset; // 2. widen through arithmetic (no overflow)
mix out{m}; // 3. narrow back via a policy at the sink
float f = double(out); // 4. leave to float only at the boundaryThe pattern is: convert in once, stay in bound-space, narrow at the sink.
Exact-rational guarantees hold throughout steps 1–3; step 4 (double(out)) is
explicit precisely because it drops the guarantee.
to<T>()— typed, fallible conversion to a native integer.numerator()/denominator()— exact read-out, no rounding.- implicit
operator imax()for integer-aligned grids (so a bound indexes an array directly). - explicit
double(b)— opt in to floating point. (real-policy math operands convert implicitly — their value is exact indouble.)
See conversions.md.
bnd::math is a <cmath>-shaped, reproducible function set over bounds —
sin/cos/sqrt/exp/log/atan2/… Math operands carry the real
policy flag (bound<G, round_nearest | real>); one API runs on either of
two engines — a fast IEEE-754 double engine (default) or a bit-exact,
constexpr integer engine (-DBOUND_MATH_FIXED=ON). Angles are radians;
output grids auto-deduce from the input; runtime-conditional failures (a
tan pole, a negative sqrt) surface as slim::expected.
See math.md.
| You want to… | Read |
|---|---|
| handle out-of-range / errors | policies.md |
| understand result-grid rules | arithmetic.md |
| convert / cast / read out | conversions.md |
| iterate, store, use with the STL | storage.md |
| call sin/cos/sqrt/… | math.md |
| know why it's shaped this way | internals.md |