Range-Validated Quantity Points¶
Physical units libraries have always been very good at preventing dimensional errors and unit mismatches. But there is a category of correctness that they have universally ignored: domain constraints on quantity point values.
A latitude is not just a length divided by a radius. It is a value that lives in \([-90°, +90°]\); anything outside that range is physically meaningless. An angle used in bearing navigation wraps cyclically around a circle; treating it as an unbounded real number ignores a fundamental property of the domain. A clinical body-temperature sensor should reject a reading of \(44\ \mathrm{°C}\) at the API boundary, not silently pass it downstream.
No units library — before this work — has provided a way to attach this kind of constraint to a quantity point at the type level, have it enforced automatically, and express different flavours (clamp, wrap, reflect, check) without any runtime polymorphism.
This article describes the motivation in depth, the design we arrived at, and the open questions we would love the community's help to answer.
The Problem¶
Coordinates and angles in practice¶
The immediate trigger for this work was a feedback during ISO C++ Committee working meeting in Croydon and a discussion opened by a user who works with geodetic coordinate systems:
How to handle type level difference in coordinate/orientation angle types?
We've now used mp-units for a long time, and while we use it for a lot of other units, we've not found it possible to use practically with basically anything that deals with world coordinates/orientation and coordinate-based angles of measure.
mp-units alone — like every other units library — had no way to encode the different wrapping disciplines that coordinate angles require.
In brief, the three families of domain constraint they needed are:
| Domain | Rule | Behavior when violated |
|---|---|---|
| Latitude / elevation on a sphere | \([-90°, +90°]\) — reflected at the poles | \(91° \to 89°\), \(270° \to -90°\) |
| Longitude / azimuth (signed convention) | \((-180°, +180°]\) — wraps cyclically | \(200° \to -160°\), \(-200° \to 160°\) |
| Longitude (positive-only convention) | \([0°, 360°)\) — wraps cyclically | \(370° \to 10°\) |
These are not the same constraint in three spellings. Mixed azimuth/bearing
systems additionally require a numeric offset (heading \(= 90° -\) geometric
azimuth), which is handled by relative_point_origin, but the range
enforcement on each origin is independent. Encoding all of this without leaking
raw-integer boilerplate into application code requires first-class support in the
library.
Domains that need this pattern¶
Geodesy is far from the only domain. Some representative examples:
| Quantity | Origin | Constraint |
|---|---|---|
| Latitude | equator | clamp or reflect to \([-90°, 90°]\) |
| Longitude | prime meridian | wrap to \((-180°, 180°]\) |
| Body temperature | clinical sensor origin | clamp to \([35°C, 42°C]\) |
| Flight altitude (MSL) | sea level | clamp to \([-500\ \mathrm{m},\ 12\,000\ \mathrm{m}]\) |
| Drone altitude (AGL) | terrain level | clamp to \([0\ \mathrm{m},\ 500\ \mathrm{m}]\) |
| Time of day | midnight | wrap to \([0\ \mathrm{s},\ 86400\ \mathrm{s})\) |
| AC thermostat setpoint | reference temperature | clamp offset to \(±3\ \mathrm{°C}\) |
| Particle phase angle | any reference | reflect at \([-π, +π]\) |
| Heading / bearing | geographic north | wrap to \((-180°, 180°]\) or \([0°, 360°)\) |
What all of these have in common is:
- There is an origin — a reference point that defines the frame of measurement.
- The displacement from that origin is physically bounded.
- The policy for handling out-of-bound values differs per domain: sometimes we clamp, sometimes we wrap, sometimes we reflect, sometimes we want a hard error.
The existing abstraction of quantity_point already captures the origin.
The missing piece is attaching a policy to the origin and having the library
enforce it automatically.
The Design¶
Bounds live on the origin, not on the type¶
The key design decision is that bounds are a property of the origin, not of the (quantity, unit, rep) triple. This follows naturally from the affine-space model: the numerical displacement from an origin is bounded by the physics of that origin's frame, not by the choice of unit or representation.
Concretely, bounds are expressed as a variable template specialization:
template<>
inline constexpr auto mp_units::quantity_bounds<equator> = mp_units::reflect_in_range{-90 * deg, 90 * deg};
This pattern mirrors how other customization points in the affine-space
API (such as
quantity_values) work. It is zero-overhead at the type level and has no
overhead at the value level beyond the enforcement call itself.
Bounds values are deltas, not points¶
A natural question to ask is: should the bounds be expressed as quantity_point
values (absolute positions) or as quantity values (displacements)?
They are displacements. The reason is architectural:
- The bounds enforcement machinery operates on a
quantity<R, Rep>(the displacementquantity_from(origin)) and returns a corrected one. Using point values would require embedding a reference origin in the bounds object, entangling the policy with the origin hierarchy it is being applied to. - Relative origins whose bounds are expressed relative to the offset also use displacements naturally — there is no notion of "absolute position" in a frame that is defined by its offset from a parent.
Four policies¶
Four concrete policies ship out of the box; all live in
<mp-units/overflow_policies.h>:
// 1. Saturate at the boundaries — silently corrects the value.
template<Quantity Q>
struct clamp_to_range { Q min; Q max; };
// 2. Cyclic wrapping into the half-open interval [min, max).
template<Quantity Q>
struct wrap_to_range { Q min; Q max; };
// 3. Bounce/fold at both boundaries — models physical reflection.
template<Quantity Q>
struct reflect_in_range { Q min; Q max; };
// 4. Report violations — delegates to the representation's error policy.
template<Quantity Q>
struct check_in_range { Q min; Q max; };
The first three are error-correcting policies: they silently adjust the value.
check_in_range is the error-reporting policy: it either calls
constraint_violation_handler<Rep>::on_violation() (when the representation type
opts into guaranteed enforcement by specializing that trait) or falls back to
MP_UNITS_EXPECTS
otherwise.
All four policies are class templates; their operator() is a function
template that accepts any compatible Quantity, so a bounds object defined with
one unit and representation works equally well regardless of the unit or
representation used by the quantity_point. For example, bounds expressed in
seconds apply transparently to a quantity_point expressed in hours or
milliseconds. This means a single quantity_bounds specialization does not need
to be repeated for every unit a user might choose.
Half-line bounds¶
Not every constraint is a closed interval. A hydraulic system that must maintain at least 50 bar above ambient to function correctly needs only a lower bound; a sensor with a ceiling and no floor needs only an upper bound.
Both cases are supported by defining a custom policy that has only a .min or
only a .max member. The library checks only the bound that is present and
leaves the other end of the range unconstrained.
// lower bound only; upper end is unconstrained
template<Quantity Q>
struct clamp_bottom {
Q min;
template<Quantity V>
constexpr V operator()(V v) const { if (v < V{min}) return V{min}; return v; }
};
// Hydraulic circuit: minimum operating pressure 50 bar above ambient; no upper cap here.
template<>
inline constexpr auto mp_units::quantity_bounds<ambient_pressure> = clamp_bottom{50 * bar};
Origin inheritance¶
A relative_point_origin that defines no own bounds automatically inherits
the enforcement from its nearest ancestor that has bounds:
// Absolute origin with physical bounds:
template<>
inline constexpr auto mp_units::quantity_bounds<prime_meridian> = wrap_to_range{-180.0 * deg, 180.0 * deg};
// Relative origin — no own bounds; inherits from prime_meridian.
// The +21° offset is transparent to the enforcement.
inline constexpr struct warsaw_meridian final : mp_units::relative_point_origin<prime_meridian + 21.0 * deg> {
} warsaw_meridian;
// A value of +200° east of Warsaw = +221° from prime → wraps to -139° from prime → -160° from Warsaw.
quantity_point qp = warsaw_meridian + 200.0 * deg;
assert(qp.quantity_from(warsaw_meridian) == -160.0 * deg);
When a relative_point_origin defines its own bounds, those bounds are
enforced directly (the tighter constraint wins). A compile-time
static_assert verifies that the relative bounds nest strictly inside the
parent's bounds:
// static_assert fires at compile time if relative bounds exceed parent bounds
template<>
inline constexpr auto mp_units::quantity_bounds<ac_setpoint> =
clamp_to_range{delta<deg_C>(-3), delta<deg_C>(+3)};
// ❌ compile error if this would violate the parent origin's physical bounds
The static_assert at definition time¶
The library validates bounds at the point where quantity_bounds<PO> is first
instantiated by enforcing the following, in order:
- The bounds object has at least one of
.minor.max— a bare{}is rejected. - For relative origins, if the parent has bounds: the relative bounds (translated by the cumulative offset) must nest strictly inside the parent's range.
Both checks are compile-time static_asserts. They fire exactly once per
specialization regardless of how many quantity_point variables are constructed.
Full example: geodetic coordinate types¶
#include <mp-units/core.h>
#include <mp-units/systems/si.h>
using namespace mp_units;
using namespace mp_units::si::unit_symbols;
inline constexpr struct geo_latitude final : quantity_spec<isq::angular_measure> {} geo_latitude;
inline constexpr struct geo_longitude final : quantity_spec<isq::angular_measure> {} geo_longitude;
// Absolute origins
inline constexpr struct equator final : absolute_point_origin<geo_latitude> {} equator;
inline constexpr struct prime_meridian final : absolute_point_origin<geo_longitude> {} prime_meridian;
Outside the anonymous namespace (so the specializations have external linkage):
template<>
inline constexpr auto mp_units::quantity_bounds<equator> =
mp_units::reflect_in_range{-90.0 * deg, 90.0 * deg}; // latitude wraps at poles
template<>
inline constexpr auto mp_units::quantity_bounds<prime_meridian> =
mp_units::wrap_to_range{-180.0 * deg, 180.0 * deg}; // longitude wraps cyclically
// Usage — bounds enforced transparently on construction and assignment.
using latitude = quantity_point<si::degree, equator, double>;
using longitude = quantity_point<si::degree, prime_meridian, double>;
constexpr latitude lat = equator + 91.0 * deg; // reflects → 89°
constexpr longitude lon = prime_meridian + 200.0 * deg; // wraps → -160°
static_assert(lat.quantity_from(equator) == 89.0 * deg);
static_assert(lon.quantity_from(prime_meridian) == -160.0 * deg);
Full example: bounded-altitude drone types¶
inline constexpr struct sea_level final : absolute_point_origin<isq::altitude> {} sea_level;
inline constexpr struct ground_level final : absolute_point_origin<isq::altitude> {} ground_level;
// MSL: physical world — flight level corridor.
template<>
inline constexpr auto mp_units::quantity_bounds<sea_level> = mp_units::clamp_to_range{-500 * m, 12'000 * m};
// AGL: operational drone envelope (non-negative).
template<>
inline constexpr auto mp_units::quantity_bounds<ground_level> = mp_units::clamp_to_range{0 * m, 500 * m};
using msl_altitude = quantity_point<altitude[m], sea_level, double>;
using agl_altitude = quantity_point<altitude[m], ground_level, double>;
constexpr auto cruising = msl_altitude{8'000.0 * m, sea_level}; // fine
constexpr auto too_high = msl_altitude{15'000.0 * m, sea_level}; // clamped to 12000 m
constexpr auto below = agl_altitude{-10.0 * m, ground_level}; // clamped to 0 m
min(), max(), and std::numeric_limits¶
When bounds are defined on the origin, the quantity_point static member
functions and the std::numeric_limits specialization reflect those bounds:
static_assert(latitude::min().quantity_from(equator) == -90.0 * deg);
static_assert(latitude::max().quantity_from(equator) == 90.0 * deg);
static_assert(std::numeric_limits<latitude>::lowest().quantity_from(equator) == -90.0 * deg);
For unbounded quantity_point types the functions are conditionally present:
min()/max() are only callable when the respective bound exists (or the
representation type provides them), and lowest() behaves like the
representation's own lowest() when there is no lower bound.
Guaranteed enforcement with constrained<T, ErrorPolicy>¶
The check_in_range policy is only as strong as the contract-checking mode in
force at the call site. By default it maps to
MP_UNITS_EXPECTS,
which in release builds may be compiled out entirely. For safety-critical code you
need guaranteed enforcement — a violation that always fires, independent of
build flags.
The constrained<T, ErrorPolicy> wrapper, provided in <mp-units/constrained.h>,
is the answer. It is a thin, transparent value wrapper around T that carries
an error policy as a type parameter. It satisfies std::regular, forwards all
arithmetic to T, and implicitly converts to and from T so it fits where T
would.
Two built-in policies ship out of the box:
| Policy | Effect on violation | Availability |
|---|---|---|
throw_policy |
throws std::domain_error |
hosted environments only |
terminate_policy |
calls std::abort() |
freestanding and hosted |
On hosted targets throw_policy is the default; on freestanding targets
terminate_policy is the default.
The connection to check_in_range is made through the
constraint_violation_handler customization point. mp-units ships a
specialization for constrained<T, EP> that delegates directly to
EP::on_constraint_violation(). This means that whenever check_in_range
would fire a contract violation it instead calls the EP — regardless of
MP_UNITS_EXPECTS settings:
// constraint_violation_handler<constrained<T, EP>> ships in constrained.h
template<typename T, typename ErrorPolicy>
struct constraint_violation_handler<constrained<T, ErrorPolicy>> {
static constexpr void on_violation(std::string_view msg) { ErrorPolicy::on_constraint_violation(msg); }
};
A concrete usage example — a body-temperature sensor that must always throw on an out-of-range reading, even in a release build.
The key design choice here is to define clinical_zero as a
relative_point_origin anchored to si::ice_point at offset 0 °C rather than
as a bare absolute_point_origin. This keeps the origin in the same hierarchy
as the rest of the Celsius/Kelvin/Fahrenheit scale, so a safe_temp value can
still be converted to Kelvin or Fahrenheit normally. At the same time the bounds
are attached only to clinical_zero, so an ordinary
quantity_point<deg_C, si::ice_point> for everyday Celsius temperatures is
completely unaffected — the library enforces the 35–42 °C constraint only where
the type says so.
#include <mp-units/core.h>
#include <mp-units/systems/si.h>
using namespace mp_units;
using namespace mp_units::si::unit_symbols;
inline constexpr struct body_temp final : quantity_spec<isq::thermodynamic_temperature> {} body_temp;
// Anchored to ice_point at 0 °C — shares the Celsius/Kelvin hierarchy,
// so quantity_point_cast to K or deg_F still works.
inline constexpr struct clinical_zero final :
relative_point_origin<point<body_temp[deg_C]>(0)> {} clinical_zero;
template<>
inline constexpr auto mp_units::quantity_bounds<clinical_zero> =
mp_units::check_in_range{delta<deg_C>(35.), delta<deg_C>(42.0)};
// Rep is constrained<double> — violations always throw std::domain_error.
using safe_temp = quantity_point<deg_C, clinical_zero, constrained<double>>;
safe_temp reading = clinical_zero + delta<deg_C>(36.6); // fine
safe_temp fever = clinical_zero + delta<deg_C>(44.0); // throws std::domain_error
Because clinical_zero is rooted in ice_point, reading.quantity_from_unit_zero()
and reading.in(deg_F) both work — the conversion offsets are known. The bounds, however,
only guard safe_temp; a plain quantity_point<deg_C, si::ice_point> used elsewhere in
the codebase is unconstrained.
Because the bounds object carries double-backed quantities and constrained<double>
satisfies the same Quantity concept requirements, the unit-flexibility
guarantees described above apply here too: you could use constrained<float> or
constrained<int> as the representation without changing the
quantity_bounds specialization.
Non-negative quantity annotations¶
Absolute quantities are quantities that live on a ratio scale — always measured from a natural zero — such as mass, duration, or electric charge magnitude. Non-negativity is the canonical constraint for all of them, and mp-units now implements it at the quantity-specification level.
The non_negative flag can be applied to any real-scalar base or named child
quantity spec, and the library propagates the flag transitively: a quantity
derived from two non-negative specs is itself non-negative.
static_assert(is_non_negative(isq::length)); // ✅ tagged in ISQ system definition
static_assert(is_non_negative(isq::mass)); // ✅ tagged in ISQ system definition
static_assert(is_non_negative(isq::speed)); // ✅ derived: length / duration
static_assert(!is_non_negative(isq::velocity)); // ❌ vector character — excluded
Note
kind_of<QS> is never non-negative, even when QS itself is tagged non_negative,
because kind_of<QS> represents the entire quantity tree including vector quantities
and signed coordinates. This matters when using CTAD with bare SI units:
When a quantity_point uses a natural_point_origin whose quantity spec is
non-negative, the library automatically attaches check_non_negative as the
bounds policy — no explicit quantity_bounds specialization is needed. The
default can always be overridden:
// Override the auto-applied check_non_negative with a clamping policy instead:
template<>
inline constexpr auto mp_units::quantity_bounds<natural_point_origin<isq::length>> = clamp_non_negative{};
Design Trade-offs and Open Questions¶
Should wrap_to_range / reflect_in_range also affect min()/max()?¶
For clamp_to_range the answer is obvious. For wrap_to_range it is less
clear: semantically the value lives on a circle so min() and max() together
define the interval, but calling either one for comparison purposes may be
misleading (there is no "smallest" longitude on a wrapped circle; they're all
equivalent modulo 360°). The current implementation does return min and max
for all policy types that expose these members.
Should quantity_bounds be applied to application-level absolute quantity ranges?¶
Beyond the automatic non-negativity enforcement described above, do you see real use cases for attaching application-specific range bounds to absolute quantities directly — for example, clamping a sensor's mass reading to its physical measurement range, or bounding a duration to a maximum scheduling window?
We Want Your Feedback¶
This feature is new in the units-library space. No prior library that we know of provides this capability. We have tried to make the design principled and composable, but there are certainly use cases we have not thought of.
If you work with geodetic coordinates, sensor data pipelines, game physics, audio DSP, or any domain where your quantity points live in a bounded or periodic domain, we would love to hear from you:
- Did the design make your use case straightforward to express?
- Were there cases where you reached for bounds but the current design would not cover them?
- Do you have a view on either of the two open questions above?
Please join the conversation in the GitHub Discussions or open a new issue.
Summary¶
| Before | After | |
|---|---|---|
| Latitude type enforces pole constraint | ❌ user responsibility | ✅ compile-time, zero-overhead |
| Longitude wraps cyclically | ❌ manual modulo | ✅ wrap_to_range on origin |
| Sensor range clamped at API boundary | ❌ runtime if-else | ✅ policy on origin |
| Relative origin inherits parent bounds | ❌ impossible | ✅ automatic, static-checked nesting |
min()/max()/numeric_limits reflect bounds |
❌ reports type limits | ✅ reports domain limits |
| Half-line (non-negative) bounds | ❌ impossible | ✅ check_non_negative / clamp_non_negative policies |
| Non-negative QS auto-guards natural origins | ❌ impossible | ✅ automatic, no specialization needed |
The implementation is already merged and covered by a comprehensive compile-time test suite. Documentation lives in the Affine Space chapter under Range-Validated Quantity Points.
References¶
- The Affine Space — Range-Validated Quantity Points — user guide section that documents the feature in full
- Ensure Ultimate Safety — how-to guide on combining
constrained<T, EP>withsafe_intfor defence-in-depth - Introducing Absolute Quantities — the companion blog post on ratio-scale non-negativity and
absolute_point_origin - Preventing Integer Overflow in Physical Computations — covers
safe_int<T>and how it composes withquantity_bounds - Understanding Safety Levels — in-depth survey of all six safety levels; Level 6 covers mathematical space safety
- GitHub Discussion #782 — the original user report on geodetic bounds that triggered this work