Skip to content

Ensure Ultimate Safety

This guide shows how to combine mp-units safety features to get guaranteed, always-on constraint enforcement for your quantities — independent of build mode or compiler optimization level.

Background reading

For in-depth background on how bounds policies and overflow handling work, see Range-Validated Quantity Points.

The Problem

With a plain representation type, a checking policy such as check_in_range makes the bounds a precondition of the operation that stores a value. Whether that precondition is checked depends on the contract configuration of the build. This is fine for catching logic errors during development, but some domains need guaranteed enforcement:

  • Safety-critical systems (aviation, medical, automotive)
  • Input validation at system boundaries
  • Financial calculations with strict ranges

The Solution

The library provides building blocks that work together:

  1. Bounds on point origins — a bounds policy passed as a template parameter to a point origin, enforcing validation on every construction and mutation
  2. constrained<T, ErrorPolicy> — a transparent wrapper that tags a representation type with an error policy
  3. constraint_violation_handler<Rep> — a customization point that library features query to dispatch errors

The library ships several overflow policies to pass as bounds template parameters: check_in_range, clamp_to_range, wrap_to_range, and reflect_in_range. This guide uses the checking policy check_in_range with a representation type that has a constraint_violation_handler. The library invokes that handler on every violation in every build mode, and the handler chooses what happens next.

Step 1: Choose an Error Policy

The library ships two error policies in <mp-units/constrained.h>:

Policy Availability Behavior
throw_policy Hosted, with exceptions enabled Throws std::domain_error
terminate_policy Always Calls std::abort()

You can also write your own:

struct log_and_continue_policy {
  static void on_constraint_violation(std::string_view msg) {
    spdlog::error("Constraint violation: {}", msg);
  }
};

Step 2: Use constrained<T, Policy> as Your Representation Type

Wrap your numeric type with the desired policy:

#include <mp-units/utility/constrained.h>

using safe_double = mp_units::utility::constrained<double, mp_units::utility::throw_policy>;

constrained<T> is transparent: it implicitly converts to/from T and forwards all arithmetic. But it carries the error policy as a compile-time tag that the library can detect.

The library automatically registers a constraint_violation_handler for every constrained<T, EP> instantiation, forwarding violations to EP::on_constraint_violation().

Step 3: Pass Bounds to Your Origin

Pass a bounds policy as a template parameter to your origin. For guaranteed enforcement, use check_in_range:

#include <mp-units/overflow_policies.h>
#include <mp-units/framework.h>
#include <mp-units/systems/si.h>

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

inline constexpr struct geo_latitude : quantity_spec<isq::angular_measure> {} geo_latitude;
inline constexpr struct equator :
    absolute_point_origin<geo_latitude, check_in_range{-90 * deg, 90 * deg}> {} equator;

Step 4: Combine Them

Use constrained<T> as the representation type in your quantity point, and validate a reading with try_make before the point exists:

using latitude = quantity_point<geo_latitude[deg], equator, safe_double>;

std::expected<latitude, bounds_violation> read_latitude(double raw_lat)
{
  return latitude::try_make(raw_lat * deg, equator);
}

An invalid reading comes back as bounds_violation, and no exception is thrown. The representation is the safety net for a violation nobody tested for, for example in arithmetic on a valid point. When check_in_range rejects such a value, the library finds the constraint_violation_handler<constrained<double, throw_policy>> specialization and calls throw_policy::on_constraint_violation("value out of bounds"), which throws std::domain_error. This happens in every build mode: debug, release, or release-with-debug-info.

Without Exceptions or Before C++23

Code built without exceptions cannot use throw_policy. Use terminate_policy as the safety net instead. The validation doesn't change, because try_make never passes a rejected value to the representation. Before C++23, where try_make isn't available, is_within_bounds applies the same test:

using checked_double = constrained<double, terminate_policy>;
using checked_latitude = quantity_point<geo_latitude[deg], equator, checked_double>;

std::optional<checked_latitude> read_latitude(double raw_lat)
{
  if (!checked_latitude::is_within_bounds(raw_lat * deg, equator)) return std::nullopt;
  return checked_latitude{raw_lat * deg, equator};
}

Both functions reject a NaN reading. See Validating Untrusted Values for the rules they follow.

Extensible policy interface

The origin bounds template parameter takes any policy that follows the interface described in Available Overflow Policies, which also covers exclusive_bound. The library ships six built-in policies — check_in_range, clamp_to_range, wrap_to_range, reflect_in_range, check_non_negative, and clamp_non_negative — and the interface is fully extensible. check_non_negative and clamp_non_negative cover the common [0, +∞) halfline case; they are also applied automatically to natural origins of non-negative ISQ quantity specs. For a fully custom one-sided or asymmetric policy, see Custom Policies. A user-defined checking policy also gets is_within_bounds and try_make.

How It Works

flowchart TB
    A["quantity_point<br>construction / mutation"] --> B["bounds policy::operator()"]
    B --> D{"Return type?"}
    D -- "bool<br>(check_in_range, check_non_negative)" --> E{"Has constraint_<br>violation_handler<br>for Rep?"}
    E -- "Yes<br>(e.g. constrained&lt;T&gt;)" --> F["handler::on_violation()<br>if the policy returns false"]
    E -- "No<br>(plain rep)" --> G["precondition of the<br>storing operation<br>(pre / contract check)"]
    F --> H["ErrorPolicy::<br>on_constraint_violation()"]
    D -- "value<br>(clamp / wrap / reflect)" --> I["Value transformed<br>(always active)"]

Bringing Your Own Handler

You don't have to use constrained<T> — any representation type can opt in by specializing constraint_violation_handler:

// Your custom safe-double type
class my_safe_double { /* ... */ };

template<>
struct mp_units::constraint_violation_handler<my_safe_double> {
  static void on_violation(std::string_view msg)
  {
    throw std::domain_error(std::string(msg));
  }
};

Now the library uses your handler whenever a checking policy, such as check_in_range, rejects the value of a quantity_point with my_safe_double rep.

Combining with safe_int

For integer quantities, the library provides safe_int<T> in <mp-units/safe_int.h>, which detects arithmetic overflow. You can use both together:

#include <mp-units/utility/safe_int.h>

// Overflow-detecting integer for arithmetic operations
quantity distance = safe_int{42} * m;  // arithmetic overflow detected at runtime

// Bounded quantity point with overflow-detecting rep
using altitude = quantity_point<isq::height[m], msl_origin, safe_int<int>>;

// Convenience aliases are available for common integer types
quantity speed = safe_i32{100} * (m / s);

safe_int also specializes constraint_violation_handler, so a check_in_range violation goes through its error policy.

See Also