Bounded Quantity Points¶
Attach domain constraints to point origins so the library enforces valid ranges automatically.
Goal: Use bounds policies on point origins to keep quantity_point values within
physical limits
Time: ~15 minutes
Prerequisites: Complete the Point Origins tutorial first
The Problem: Invalid Values at the Wrong Layer¶
You have learned how to define custom point origins. But many of those origins have physically meaningful bounds. A drone's altitude above ground cannot be negative. A latitude cannot exceed ±90°. A body-temperature sensor should reject 51 °C at the API boundary, not silently propagate it downstream.
Without additional tooling every function that receives a quantity_point would need
its own range check — and those checks would inevitably be duplicated, forgotten, or
bypassed. mp-units lets you attach the constraint once to the origin and rely on
the library to enforce it.
Passing Bounds to Point Origins¶
Pass a bounds policy as a template parameter to your origin to attach range validation:
#include <mp-units/core.h>
inline constexpr struct your_origin :
absolute_point_origin<your_quantity_spec, clamp_to_range{min_val, max_val}> {} your_origin;
The library calls the policy automatically every time a quantity_point bound to that
origin is constructed or mutated. No if-else in application code is needed.
Six Built-In Policies¶
Six concrete policies are available, all in <mp-units/overflow_policies.h>:
| Policy | Behaviour |
|---|---|
clamp_to_range{min, max} |
Saturate to nearest boundary (silent correction) |
wrap_to_range{min, max} |
Cyclic modulo into [min, max), e.g. angles |
reflect_in_range{min, max} |
Bounce/fold at both boundaries, e.g. latitude |
check_in_range{min, max} |
Test [min, max], enforced by the handler or as a precondition |
check_non_negative{} |
Test >= 0, like check_in_range |
clamp_non_negative{} |
Saturate to zero for values below zero (tolerates rounding noise) |
Bounds are expressed as quantity displacements from the origin, not as absolute
quantity_point values. A policy written in one unit works equally for any unit the
quantity_point uses — bounds in metres apply transparently to a point in kilometres.
Wrapping a bound in exclusive_bound excludes it from the range, as in
check_in_range{0 * deg, exclusive_bound{360 * deg}}. check_in_range accepts it at
either end, and wrap_to_range at its max.
The last two policies in the table are half-line policies for inherently non-negative
domains. They carry no upper bound — only a min member (the natural zero).
In general, whenever a policy exposes a min or max member, quantity_point::min(),
quantity_point::max(), and the corresponding std::numeric_limits functions reflect
those bounds instead of the representation type's own extremes. No min() or max() is
offered for an excluded end, such as the max of wrap_to_range.
Try It: Drone Altitude Zones¶
// ce-embed height=880 compiler=clang2110 flags="-std=c++23 -stdlib=libc++ -O3" mp-units=trunk
#include <mp-units/core.h>
#include <mp-units/systems/isq.h>
#include <mp-units/systems/si.h>
#include <iostream>
using namespace mp_units;
using namespace mp_units::si::unit_symbols;
inline constexpr struct sea_level :
absolute_point_origin<isq::altitude, clamp_to_range{-500 * m, 12'000 * m}> {} sea_level;
inline constexpr struct ground_level :
absolute_point_origin<isq::altitude, clamp_to_range{0 * m, 500 * m}> {} ground_level;
int main()
{
quantity_point cruise = sea_level + 8'500.0 * m;
quantity_point too_high = sea_level + 15'000.0 * m; // clamped!
quantity_point hover = ground_level + 50.0 * m;
quantity_point below = ground_level - 10.0 * m; // clamped!
std::cout << "Cruise MSL: " << (cruise - sea_level) << "\n"; // 8500 m
std::cout << "Too-high: " << (too_high - sea_level) << "\n"; // 12000 m ← clamped
std::cout << "Hover AGL: " << (hover - ground_level) << "\n"; // 50 m
std::cout << "Below AGL: " << (below - ground_level) << "\n"; // 0 m ← clamped
}
Key insight: Bounds are enforced at the point of construction. too_high is never
15'000 m — it is already clamped to 12'000 m at the moment it is first assigned.
min() and max() Reflect the Bounds¶
When bounds are defined the quantity_point type exposes static min() and max()
functions and a matching std::numeric_limits specialization:
using msl = quantity_point<si::metre, sea_level, double>;
static_assert(msl::min().quantity_from(sea_level) == -500.0 * m);
static_assert(msl::max().quantity_from(sea_level) == 12'000.0 * m);
Without bounds, min() and max() are only available if the representation type itself
provides them.
Origin Inheritance¶
A relative_point_origin that defines no own bounds automatically inherits the
enforcement of its nearest bounded ancestor. When own bounds are provided they are checked
at compile time to nest inside that ancestor's range:
// Relative origin — no own bounds; inherits clamping from sea_level.
inline constexpr struct airport_elevation : relative_point_origin<sea_level + 200 * m> {} airport_elevation;
// Value 12100 m MSL would exceed the sea_level cap of 12000 m → clamped.
quantity_point takeoff = airport_elevation + 11'900.0 * m;
Automatic Bounds for Non-Negative Quantities¶
Many quantity specifications are inherently non-negative — length, mass, duration,
speed, area, etc. The ISQ base quantities isq::length, isq::mass, isq::duration,
isq::thermodynamic_temperature, isq::amount_of_substance, and isq::luminous_intensity
are all tagged non_negative in the library. The library tracks this through the
is_non_negative() query:
static_assert(is_non_negative(isq::length)); // ✅ tagged in ISQ
static_assert(is_non_negative(isq::mass)); // ✅ tagged in ISQ
static_assert(is_non_negative(isq::speed)); // ✅ derived: length / duration
static_assert(!is_non_negative(isq::displacement)); // ❌ vector character — excluded
When a quantity_point uses a natural_point_origin whose quantity spec is non-negative,
the library automatically attaches check_non_negative — no explicit bounds
definition is required:
// ce-embed compiler=clang2110 flags="-std=c++23 -stdlib=libc++ -O3" mp-units=trunk
#include <mp-units/systems/isq.h>
#include <mp-units/systems/si.h>
#include <iostream>
using namespace mp_units;
using namespace mp_units::si::unit_symbols;
inline constexpr struct distance_traveled : quantity_spec<isq::length> {} distance_traveled;
int main()
{
// natural_point_origin<distance_traveled> is auto-protected by check_non_negative
// because it inherits non_negative from isq::length.
quantity_point start(distance_traveled(0.0 * m)); // fine: 0 m is the boundary
quantity_point finish(distance_traveled(5.0 * km)); // fine: 5 km ≥ 0
std::cout << "Start: " << start << "\n";
std::cout << "Finish: " << finish << "\n";
// quantity_point bad(distance_traveled(-1.0 * cm)); // ❌ precondition violation
// std::cout << "Bad: " << bad << "\n";
}
You can override the default by defining a custom origin with different bounds. For example, to silently clamp rounding-noise negatives in a computed result rather than failing:
inline constexpr struct clamped_distance_origin :
absolute_point_origin<distance_traveled, clamp_non_negative{}> {} clamped_distance_origin;
Non-Negative Bounds Through Relative Origins¶
The same inheritance applies when the parent bounds come from the automatic
check_non_negative protection. A relative origin sitting above the ground
floor may hold negative values — as long as the absolute position stays ≥ 0:
// inherits non_negative from ISQ
inline constexpr struct height_spec : quantity_spec<isq::height> {} height_spec;
// natural_point_origin<height_spec> auto-gets check_non_negative — no explicit bounds needed.
// The relative origin at +1700 m inherits that constraint.
inline constexpr struct average_height_origin :
relative_point_origin<natural_point_origin<height_spec> + 1700.0 * m> {} average_height_origin;
// −1500 m relative = 200 m absolute (≥ 0) → valid.
quantity_point low = average_height_origin - 1500.0 * m;
// −1700 m relative = 0 m absolute → valid (boundary).
quantity_point ground = average_height_origin - 1700.0 * m;
// −1701 m relative = −1 m absolute → check_non_negative fires.
// quantity_point bad = average_height_origin - 1701.0 * m; // ❌
The enforcement is a single flat translation: the library converts the relative value
to the absolute frame and tests it with check_non_negative there. The stored value
is always the original one (no clamping). With a plain double, a value outside the
bounds violates the
precondition
of the operation that stores it. A constrained<T, Policy> representation passes it to
its error policy in every build mode.
Kinds are never auto-bounded¶
kind_of<QS> represents the entire quantity tree rooted at QS — including vector
quantities and signed coordinates. It is therefore never non_negative, even when QS
itself is:
This matters in practice because CTAD from a bare SI unit deduces kind_of:
// quantity_point{5.0 * m} has origin = natural_point_origin<kind_of<isq::length>>.
// isq::length is tagged non_negative, but that origin is NOT auto-bounded because
// kind_of<isq::length> includes displacement and other signed/vector subtypes.
quantity_point generic_len(5.0 * m); // ← no auto-check, kind_of origin
quantity_point dist(distance_traveled(5.0 * m)); // ← auto-checked, named spec
Always use an explicit quantity reference when you need the auto-protection guarantee.
Challenges¶
-
Coordinate system: Define an
equatorabsolute origin forisq::angular_measure. Attachreflect_in_range{-90 * deg, 90 * deg}and verify that constructing a point at 95° gives 85°. -
Wrapping longitude: Define a
prime_meridianabsolute origin. Attachwrap_to_range{-180.0 * deg, 180.0 * deg}and verify that 200° wraps to −160°. -
Nested bounds: Define
airport_elevasrelative_point_origin<sea_level + 1'000 * m>. Attachclamp_to_range{-50 * m, 4'000 * m}toairport_elev. Verify that 4100 m above the airport clamps to 4000 m.
What You Learned¶
✅ Passing a bounds policy as a template parameter to a point origin attaches validation
once
✅ Six built-in policies: clamp_to_range, wrap_to_range, reflect_in_range,
check_in_range, check_non_negative, clamp_non_negative
✅ Bounds are expressed as quantity displacements — independent of unit or representation
✅ Relative origins inherit bounds from the nearest ancestor that has them
✅ Non-negative ISQ specs automatically guard their natural origins with
check_non_negative
✅ You can define custom origins with different bounds to override the defaults
Next Steps¶
Ready for more control? Learn how to customize what happens when bounds are violated:
- Tutorial: Custom Contract Handlers - Implement custom error policies
(logging, recovery, auditing) for
constrained<T, ErrorPolicy>representation types - Workshop: Bounds Validation - Practice building a weather monitoring system with custom error handling and violation tracking