Skip to content

Bounded Quantity Points

Attach domain constraints to point origins so the library enforces valid ranges automatically.

Goal: Use quantity_bounds 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.

The quantity_bounds Customization Point

Specialize quantity_bounds at namespace scope (outside any function) for your origin to attach a bounds policy:

#include <mp-units/core.h>

template<>
inline constexpr auto mp_units::quantity_bounds<your_origin> = mp_units::clamp_to_range{min_val, max_val};

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} Report a violation via contract / handler
check_non_negative{} Report a violation when the value drops below zero
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.

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

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/si.h>
#include <iostream>

using namespace mp_units;
using namespace mp_units::si::unit_symbols;

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> = clamp_to_range{-500 * m, 12'000 * m};

// AGL: drone operational envelope [0 m, 500 m].
template<>
inline constexpr auto mp_units::quantity_bounds<ground_level> = clamp_to_range{0 * m, 500 * m};

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 parent's enforcement. When own bounds are provided they are checked at compile time to nest strictly inside the parent's range:

// Relative origin — no own bounds; inherits clamping from sea_level.
inline constexpr struct airport_elevation final : 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 quantity_bounds specialization 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 final : 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.quantity_from_unit_zero() << "\n";
  std::cout << "Finish: " << finish.quantity_from_unit_zero() << "\n";

  // quantity_point bad(distance_traveled(-1.0 * cm));  // ❌ contract violation
  // std::cout << "Bad:    " << bad.quantity_from_unit_zero() << "\n";
}

You can override the default with your own specialization. For example, to silently clamp rounding-noise negatives in a computed result rather than failing:

template<>
inline constexpr auto mp_units::quantity_bounds<natural_point_origin<distance_traveled>> = clamp_non_negative{};

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:

static_assert(!is_non_negative(kind_of<isq::length>));  // kind encompasses signed subtypes

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

  1. Coordinate system: Define an equator absolute origin for isq::angular_measure. Attach reflect_in_range{-90 * deg, 90 * deg} and verify that constructing a point at 95° gives 85°.

  2. Wrapping longitude: Define a prime_meridian absolute origin. Attach wrap_to_range{-180.0 * deg, 180.0 * deg} and verify that 200° wraps to −160°.

  3. Nested bounds: Define airport_elev as relative_point_origin<sea_level + 1'000 * m>. Attach clamp_to_range{-50 * m, 4'000 * m} to airport_elev. Verify that 4100 m above the airport clamps to 4000 m.

What You Learned?

quantity_bounds<origin> attaches a validation policy to a point origin 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
✅ Any default can be overridden with a user-supplied quantity_bounds specialization

Next Steps

Ready for more control? Learn how to customize what happens when bounds are violated: