safe_int<T> — Overflow-Safe Integer Arithmetic¶
Overview¶
safe_int<T> is a thin wrapper around an integral type T that detects all arithmetic
overflow at runtime. It participates in the ordinary C++ arithmetic system and satisfies
all the concepts that mp-units requires of a
representation type — making it a true drop-in replacement for
plain integers.
#include <mp-units/safe_int.h>
using namespace mp_units;
// Just change the representation type — everything else stays the same
quantity<mm, safe_i32> distance{1'500 * km};
quantity<mm, safe_i32> doubled = distance + distance; // throws — 3×10⁹ > INT32_MAX
Motivation
mp-units' built-in scaling algorithm
uses widened intermediate arithmetic (int64_t for types up to int32_t, 128-bit for
int64_t) to avoid undefined behavior during unit conversions. This handles the vast
majority of real-world scenarios, but the final result must still fit in the target
type — and that narrowing can overflow silently.
safe_int<T> closes this gap: it checks every arithmetic operation, including
the final narrowing, so overflow is never silent.
Checked operations¶
Every arithmetic operation on safe_int<T> is checked before it executes:
| Operation | Overflow condition checked |
|---|---|
a + b |
signed: both-same-sign sum crosses boundary; unsigned: lhs > max - rhs |
a - b |
symmetric to addition |
a * b |
widened multiplication; result outside [min, max] |
a / b |
divide-by-zero; signed: INT_MIN / -1 |
-a |
signed: INT_MIN; unsigned: any non-zero |
The multiplication check uses widened intermediate arithmetic (e.g., int32_t promotes to
int64_t for the product), so there is no dependency on undefined behavior.
Policy-based error handling¶
What happens when overflow is detected is controlled by the ErrorPolicy template
parameter:
mp-units ships two policies:
| Policy | Behaviour | Environment |
|---|---|---|
safe_int_terminate_policy |
std::abort() immediately |
freestanding + hosted |
safe_int_throw_policy |
throws std::overflow_error |
hosted only |
The default policy is safe_int_throw_policy on hosted platforms and
safe_int_terminate_policy on freestanding platforms.
Convenience aliases¶
All standard fixed-width integer aliases are provided with the default policy:
using safe_i8 = mp_units::safe_int<std::int8_t>;
using safe_i16 = mp_units::safe_int<std::int16_t>;
using safe_i32 = mp_units::safe_int<std::int32_t>;
using safe_i64 = mp_units::safe_int<std::int64_t>;
using safe_u8 = mp_units::safe_int<std::uint8_t>;
using safe_u16 = mp_units::safe_int<std::uint16_t>;
using safe_u32 = mp_units::safe_int<std::uint32_t>;
using safe_u64 = mp_units::safe_int<std::uint64_t>;
For explicit policy control, use the full template:
safe_int<std::int32_t, safe_int_throw_policy> explicit_throw;
safe_int<std::int32_t, safe_int_terminate_policy> explicit_terminate;
Custom error policies¶
You can define your own error policy to integrate with custom logging or diagnostics systems:
#include <mp-units/safe_int.h>
struct logging_policy {
[[noreturn]] static void on_overflow(std::string_view msg)
{
log_critical_error("Arithmetic overflow", msg);
std::abort();
}
};
using logged_int = mp_units::safe_int<std::int32_t, logging_policy>;
The policy must provide a static void on_overflow(std::string_view) method.
Shared policies with constrained<T>
If you also use
quantity_bounds for range
validation, you can add on_constraint_violation(std::string_view) to the same
policy type — both safe_int and constrained will use it. See
Ensure Ultimate Safety for
a complete example.
Integral promotion rules¶
safe_int<T> preserves C++ integral promotion behavior — adding two safe_int<int16_t>
values produces safe_int<int>, exactly matching what happens with the underlying types:
// Underlying types: int16_t + int16_t → int (integral promotion)
static_assert(std::is_same_v<decltype(std::int16_t{1} + std::int16_t{1}), int>);
// safe_int preserves this: safe_int<int16_t> + safe_int<int16_t> → safe_int<int>
static_assert(std::is_same_v<decltype(safe_i16{1} + safe_i16{1}), safe_int<int>>);
// This propagates through quantity arithmetic:
static_assert(std::is_same_v<decltype(safe_i16{1} * si::metre + safe_i16{1} * si::metre),
quantity<si::metre, safe_int<int>>>);
This ensures that safe_int acts as a transparent wrapper — it adds overflow detection
without changing the fundamental arithmetic behavior.
Drop-in replacement¶
safe_int<T> satisfies all the same representation concepts
as T. Only the representation type changes — everything else stays identical:
The overflow is caught because q + q promotes to safe_int<int> via integral promotion
(just as int16_t + int16_t → int), and the quantity<si::metre, safe_i16> constructor
narrows the result back to int16_t — that narrowing is where safe_int detects that
60,000 doesn't fit and throws.
Where overflow is caught?¶
Because safe_int hooks into the fundamental C++ arithmetic operators, every operation
is checked — regardless of context:
// Same-unit addition — no unit conversion, plain safe_int arithmetic
quantity dist = safe_i32{2'000'000'000} * si::metre;
quantity total = dist + dist; // throws — 4×10⁹ overflows int32_t
// Cross-quantity multiplication: speed × time
quantity speed = safe_i32{50'000} * (si::metre / si::second);
quantity time = safe_i32{50'000} * si::second;
quantity distance = speed * time; // throws — 2.5×10⁹ overflows int32_t
Comparisons¶
Comparison operators (==, <, >, etc.) are structurally safer than arithmetic: they
return only bool, so the widened intermediate values are used to produce the boolean
result and then discarded. mp-units widens the intermediate freely (to int64_t or
128-bit), so for the vast majority of practical scenarios comparisons are correct with
plain integer types — safe_int adds no extra benefit here.
For extreme corner cases where even 128-bit intermediates overflow, see the static_assert
examples in the
Preventing Integer Overflow
blog post.
constexpr support¶
safe_int<T> arithmetic is fully constexpr. In C++, any overflow that occurs during
constant expression evaluation is always a compile-time hard error — for both safe_int
and plain integers. The difference emerges only at runtime, where safe_int catches
overflows that plain integers silently ignore.
Relation to constrained<T, Policy>¶
safe_int<T, EP> follows the same policy-based design as
constrained<T, EP>, the wrapper
used for quantity_point domain bounds. They address complementary concerns:
| Wrapper | Purpose |
|---|---|
constrained<T, EP> |
Value domain — is this value within the declared bounds? |
safe_int<T, EP> |
Arithmetic correctness — does this operation fit in the type? |
Both share the same base error policies (throw_policy, terminate_policy) and extend
them with domain-specific handlers (on_constraint_violation for constrained,
on_overflow for safe_int). They compose naturally:
// A bounded, overflow-safe latitude representation
using safe_double = constrained<double, throw_policy>;
using safe_latitude = quantity_point<geo_latitude[deg], equator, safe_double>;
// A raw-integral distance type where overflow is caught at runtime
using safe_i32_m = quantity<isq::length[si::metre], safe_int<std::int32_t>>;
For a complete walkthrough of combining both wrappers, see Ensure Ultimate Safety.