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 (e.g. int64_t/uint64_t for types up to
int32_t/uint32_t, 128-bit for 64-bit types) 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.
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<mm, safe_i32> dist{1'500 * km};
quantity<mm, safe_i32> total = dist + dist; // throws — 3×10⁹ > INT32_MAX
// 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
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) member function.
Shared policies with constrained<T>
The built-in safe_int_throw_policy and safe_int_terminate_policy already inherit
from throw_policy / terminate_policy, so they satisfy both OverflowPolicy and
ConstraintPolicy out of the box. When writing a custom policy, add both
on_overflow(std::string_view) and on_constraint_violation(std::string_view) to the
same struct, and it will work with safe_int and constrained alike. See
Relation to constrained<T, Policy> and
Ensure Ultimate Safety for
a complete example.
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.
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.
Arithmetic result types¶
The result type of any safe_int arithmetic expression is fully determined by the operand
types. The subsections below cover all supported combinations; unsupported pairings are
compile-time errors.
Integral promotion rules¶
safe_int<T> preserves C++ integral promotion behavior — this applies to both the
homogeneous case (two safe_int<T> operands) and the scalar case (one safe_int<T> and
one plain integral):
// 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>);
// Homogeneous: 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>>);
// Scalar path: safe_int<int16_t> + int16_t → safe_int<int> (same promotion)
static_assert(std::is_same_v<decltype(safe_i16{1} + std::int16_t{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.
Arithmetic operators (+, -, *, /, %)¶
The following tables use this notation:
S<T, EP>—safe_int<T, EP>, whereEPis the policy of thesafe_intoperandC<U, CP>—constrained<U, CP>, whereCPis the policy of theconstrainedoperand- promote(T, U) — the type produced by
T{} + U{}under C++ usual arithmetic conversions (integral promotion + common type), as illustrated above
| Left operand | Right operand | Condition | Result type | Overflow check |
|---|---|---|---|---|
S<T, EP> |
S<T, EP> |
same T, same EP |
S<promote(T,T), EP> |
yes — every operation |
S<T, EP> |
S<U, EP> |
same-sign, different width, same EP |
S<promote(T,U), EP> |
yes — narrower widens implicitly, then checked |
S<T, EP> |
S<U, EP> |
opposite signedness | ill-formed | — |
S<T, EP1> |
S<T, EP2> |
different EP |
ill-formed | — |
S<T, EP> |
integral U |
same-signedness | S<promote(T,U), EP> |
yes — every operation |
S<T, EP> |
integral U |
opposite signedness | ill-formed | — |
S<T, EP> |
floating-point U |
— | plain U (floating-point) |
no — safe_int wrapper is lost |
S<T, EP> |
C<U, CP> (U integral, same-sign) |
— | S<promote(T,U), EP> |
yes — safe_int wins; policy CP is dropped |
S<T, EP> |
C<U, CP> (U non-integral / floating) |
— | C<U, CP> |
no — constrained wins; policy CP is preserved |
safe_int + float loses overflow protection
When a safe_int operand is combined with a floating-point type, the result is a
plain floating-point value — the safe_int wrapper is not preserved. This is
intentional: floating-point arithmetic has its own overflow semantics (infinity / NaN)
that are incompatible with the binary-integer overflow model.
safe_int wins over constrained<integral>
When a safe_int meets a constrained<U> wrapping an integral U, the result
is safe_int and the constrained policy CP is dropped. The rationale is that
constrained is a transparent policy-carrying tag — it performs no checks itself;
safe_int is the type that actively checks arithmetic, so it is the more meaningful
wrapper to preserve.
When U is a non-integral type (e.g. double inside constrained), the result
is constrained because the safe_int overflow model does not apply to floating-point.
Mixed-signedness arithmetic¶
Arithmetic between opposite signedness is intentionally ill-formed — a compile-time
error. This applies to both safe_int×safe_int and safe_int×scalar combinations.
The rationale is the same in both cases: mixed-signedness arithmetic under C++ usual
arithmetic conversions reinterprets the signed value as unsigned before operating, silently
producing counterintuitive results (e.g., safe_int<int>{-1} * 2u → UINT_MAX - 1).
safe_int rejects these outright rather than hiding the problem behind an overflow check.
Comparisons between mixed-signedness values remain allowed — both when the right-hand side
is a raw integral scalar and when it is another safe_int of opposite signedness. The
comparison operators use std::cmp_equal / std::cmp_less etc., which correctly handle
mixed-signedness without reinterpretation.
safe_int<int> si{1};
safe_int<unsigned> su{2u};
// Compile-time error — two safe_int of opposite signedness:
// auto x = si + su;
// Compile-time error — safe_int<signed> × unsigned scalar:
// auto y = si * 2u;
// Explicit mixed-signedness arithmetic — use .value() and cast deliberately:
auto x = safe_int<unsigned>{static_cast<unsigned>(si.value())} + su; // explicit and auditable
// Comparison is allowed for both safe_int pairs and scalars (uses std::cmp_less):
bool b1 = si < su; // OK — safe_int × safe_int cross-sign comparison
bool b2 = si < 2u; // OK — safe_int × scalar cross-sign comparison
Comparisons¶
Comparison operators never overflow — they produce a bool, not a numeric value, so there
is nothing to overflow. safe_int therefore adds no overflow-detection benefit for
comparisons; its contribution is elsewhere: the mixed-signedness operators use std::cmp_*
instead of C++ usual arithmetic conversions, which gives the correct result where raw
integers would silently mislead.
Comparison operators (==, !=, <, >, <=, >=)¶
| Left operand | Right operand | Condition | Result | Notes |
|---|---|---|---|---|
S<T, EP> |
S<T, EP> |
same T, same EP |
bool |
direct value comparison |
S<T, EP> |
S<U, EP> |
same-sign, same EP |
bool |
narrower widens implicitly, then compared |
S<T, EP1> |
S<U, EP2> |
same-sign, different EP |
bool |
comparisons yield bool — no result policy to propagate |
S<signed T> |
S<unsigned U> |
opposite signedness, any EP |
bool |
uses std::cmp_equal / std::cmp_less — cross-sign is correct and allowed |
S<T, EP> |
integral U |
any sign combination | bool |
uses std::cmp_equal / std::cmp_less — cross-sign is correct and allowed |
S<T, EP> |
floating-point U |
— | bool |
standard floating-point comparison |
S<T, EP> |
C<U, CP> |
— | bool |
compares underlying values directly |
Cross-sign comparison is intentionally allowed
All safe_int comparison operators — whether comparing against a raw integral scalar or
against another safe_int of opposite signedness — use std::cmp_equal / std::cmp_less
(and friends) rather than C++ usual arithmetic conversions. This gives the mathematically
correct result regardless of signedness, unlike the raw int < unsigned int which silently
reinterprets the signed value.
This asymmetry (arithmetic: ill-formed; comparison: allowed and correct) is
deliberate: arithmetic under mixed-sign UAC produces wrong values, while
comparisons using std::cmp_* produce the correct bool.
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 the
relationship is explicit inheritance:
classDiagram
class terminate_policy {
+on_constraint_violation(string_view)
}
class throw_policy {
+on_constraint_violation(string_view)
}
class safe_int_terminate_policy {
+on_overflow(string_view)
}
class safe_int_throw_policy {
+on_overflow(string_view)
}
terminate_policy <|-- safe_int_terminate_policy
throw_policy <|-- safe_int_throw_policy
This means safe_int_throw_policy and safe_int_terminate_policy satisfy both
OverflowPolicy and ConstraintPolicy. As a result, swapping constrained<T, EP> for
safe_int<T, EP> using one of the built-in policies preserves any quantity_point bounds
checking that was already in place — the on_constraint_violation handler is inherited and
still fires when a domain bound is violated.
// 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.