Files
mp-units/test/runtime/safe_int_test.cpp
Mateusz Pusz 7a03123a01 refactor(utility): move safe_int and constrained into the mp_units::utility namespace
safe_int, constrained, their error policies, and the safe_iN aliases are
non-framework add-ons, so their public names move out of mp_units into the
mp_units::utility extension tier (next to the representation concepts already
there). The headers move to core/include/mp-units/utility/ accordingly but stay
in the core component: safe_int reuses the core 128-bit integer toolkit
(integral, is_signed_v, int128_t, ...) that also backs the scaling engine, so it
bridges to mp_units::detail with a single using-directive rather than relocating
or duplicating that toolkit. overflow_policies stays framework (it backs bounded
quantity point origins). No deprecation shims: these types are unreleased.

Also fixes utility/representation.h, whose public concepts delegated via the
unqualified detail::X. That resolved to mp_units::detail only as long as
mp_units::utility::detail did not exist; now that constrained/safe_int introduce
it, the references are qualified to ::mp_units::detail:: so they stay
order-independent (this surfaced only in the module build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 23:58:39 +02:00

394 lines
14 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The MIT License (MIT)
//
// Copyright (c) 2018 Mateusz Pusz
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#include <catch2/catch_test_macros.hpp>
#include <mp-units/bits/hacks.h>
#include <mp-units/systems/isq/space_and_time.h>
#include <mp-units/systems/si.h>
#include <mp-units/systems/yard_pound.h>
#include <mp-units/utility/constrained.h>
#include <mp-units/utility/safe_int.h>
#ifdef MP_UNITS_IMPORT_STD
import std;
#else
#include <cstdint>
#include <limits>
#include <stdexcept>
#include <string>
#endif
using namespace mp_units;
using namespace mp_units::utility;
using namespace mp_units::si::unit_symbols;
using namespace mp_units::yard_pound::unit_symbols;
// ============================================================================
// safe_int with throw_policy (default on hosted)
// ============================================================================
TEST_CASE("safe_int with throw_policy", "[safe_int][throw]")
{
using T = safe_int<int>;
const auto int_max = std::numeric_limits<int>::max();
const auto int_min = std::numeric_limits<int>::min();
SECTION("normal arithmetic does not throw")
{
REQUIRE(T{3} + T{4} == T{7});
REQUIRE(T{10} - T{3} == T{7});
REQUIRE(T{3} * T{4} == T{12});
REQUIRE(T{12} / T{4} == T{3});
REQUIRE(T{10} % T{3} == T{1});
REQUIRE(-T{5} == T{-5});
T x{5};
REQUIRE(++x == T{6});
REQUIRE(x++ == T{6});
REQUIRE(x == T{7});
REQUIRE(--x == T{6});
REQUIRE(x-- == T{6});
REQUIRE(x == T{5});
}
SECTION("addition overflow throws")
{
REQUIRE_THROWS_AS(T{int_max} + T{1}, std::overflow_error);
REQUIRE_THROWS_AS(T{int_min} + T{-1}, std::overflow_error);
}
SECTION("subtraction overflow throws")
{
REQUIRE_THROWS_AS(T{int_min} - T{1}, std::overflow_error);
REQUIRE_THROWS_AS(T{int_max} - T{-1}, std::overflow_error);
}
SECTION("multiplication overflow throws")
{
REQUIRE_THROWS_AS(T{int_max} * T{2}, std::overflow_error);
REQUIRE_THROWS_AS(T{int_min} * T{-1}, std::overflow_error);
}
SECTION("division overflow throws")
{
REQUIRE_THROWS_AS(T{1} / T{0}, std::overflow_error);
REQUIRE_THROWS_AS(T{int_min} / T{-1}, std::overflow_error);
}
SECTION("modulo by zero throws") { REQUIRE_THROWS_AS(T{10} % T{0}, std::overflow_error); }
SECTION("negation overflow throws") { REQUIRE_THROWS_AS(-T{int_min}, std::overflow_error); }
SECTION("negation of short INT16_MIN does not overflow (promotes to int)")
{
// -(-32768) as short would overflow short, but unary - promotes to int first,
// so the result is int(32768) which is in range — no exception expected.
using S = safe_int<std::int16_t>;
REQUIRE(-S{std::numeric_limits<std::int16_t>::min()} == safe_int<int>{32768});
}
SECTION("increment overflow throws")
{
T x{int_max};
REQUIRE_THROWS_AS(++x, std::overflow_error);
}
SECTION("decrement overflow throws")
{
T x{int_min};
REQUIRE_THROWS_AS(--x, std::overflow_error);
}
SECTION("unsigned overflow throws")
{
using U = safe_int<unsigned>;
const auto uint_max = std::numeric_limits<unsigned>::max();
REQUIRE_THROWS_AS(U{uint_max} + U{1u}, std::overflow_error);
REQUIRE_THROWS_AS(U{0u} - U{1u}, std::overflow_error);
REQUIRE_THROWS_AS(U{uint_max} * U{2u}, std::overflow_error);
}
SECTION("throw message contains 'overflow'")
{
try {
[[maybe_unused]] auto r = T{int_max} + T{1};
FAIL("expected exception not thrown");
} catch (const std::overflow_error& e) {
REQUIRE(std::string(e.what()).find("overflow") != std::string::npos);
}
}
}
// ============================================================================
// Converting constructor overflow detection
// ============================================================================
TEST_CASE("safe_int converting constructor", "[safe_int][convert][throw]")
{
SECTION("raw integer narrowing overflow")
{
// int → short: value too large
REQUIRE_THROWS_AS(safe_int<short>(int{40'000}), std::overflow_error);
REQUIRE_THROWS_AS(safe_int<short>(int{-40'000}), std::overflow_error);
// int → short: value fits — no throw
REQUIRE(safe_int<short>(int{100}).value() == 100);
REQUIRE(safe_int<short>(int{-100}).value() == -100);
REQUIRE(safe_int<short>(int{32'767}).value() == 32'767);
REQUIRE(safe_int<short>(int{-32'768}).value() == -32'768);
// unsigned → short: value too large
REQUIRE_THROWS_AS(safe_int<short>(40'000u), std::overflow_error);
// int → uint8_t: value out of range
REQUIRE_THROWS_AS(safe_int<std::uint8_t>(int{256}), std::overflow_error);
REQUIRE_THROWS_AS(safe_int<std::uint8_t>(int{-1}), std::overflow_error);
// int → unsigned: negative value
REQUIRE_THROWS_AS(safe_int<unsigned>(int{-1}), std::overflow_error);
// unsigned → int: value too large
REQUIRE_THROWS_AS(safe_int<int>(std::numeric_limits<unsigned>::max()), std::overflow_error);
}
SECTION("safe_int<U> narrowing overflow")
{
// safe_int<int> → safe_int<short>: value too large
REQUIRE_THROWS_AS(safe_int<short>(safe_int<int>{40'000}), std::overflow_error);
REQUIRE_THROWS_AS(safe_int<short>(safe_int<int>{-40'000}), std::overflow_error);
// safe_int<int> → safe_int<short>: value fits — no throw
REQUIRE(safe_int<short>(safe_int<int>{100}).value() == 100);
// safe_int<unsigned> → safe_int<int>: value too large
REQUIRE_THROWS_AS(safe_int<int>(safe_int<unsigned>{std::numeric_limits<unsigned>::max()}), std::overflow_error);
// safe_int<int> → safe_int<unsigned>: negative value
REQUIRE_THROWS_AS(safe_int<unsigned>(safe_int<int>{-1}), std::overflow_error);
}
}
// ============================================================================
// safe_int as a quantity representation type
// ============================================================================
TEST_CASE("safe_int as quantity representation", "[safe_int][quantity]")
{
SECTION("basic arithmetic does not throw")
{
quantity<isq::length[m], safe_int<int>> d1 = safe_int{100} * m;
quantity<isq::length[m], safe_int<int>> d2 = safe_int{50} * m;
REQUIRE((d1 + d2).numerical_value_in(m) == 150);
REQUIRE((d1 - d2).numerical_value_in(m) == 50);
REQUIRE((d1 * 2).numerical_value_in(m) == 200);
REQUIRE((d1 / 2).numerical_value_in(m) == 50);
}
SECTION("unit conversion does not throw when safe")
{
quantity<isq::length[m], safe_int<int>> distance = safe_int{2} * m;
quantity<isq::length[mm], safe_int<int>> distance_mm = distance;
REQUIRE(distance_mm.numerical_value_in(mm) == 2000);
}
SECTION("arithmetic overflow throws")
{
quantity<isq::length[m], safe_int<int>> large = safe_int{std::numeric_limits<int>::max()} * m;
REQUIRE_THROWS_AS(large + safe_int{1} * m, std::overflow_error);
}
SECTION("cross-unit arithmetic")
{
quantity<isq::length[m], safe_int<std::int16_t>> meters = safe_int<std::int16_t>{20} * m;
quantity<isq::length[ft], safe_int<std::int16_t>> feet = safe_int<std::int16_t>{100} * ft;
auto total = meters + feet;
REQUIRE(total.force_in(m).numerical_value_in(m) > 0);
}
SECTION("cross-unit overflow throws")
{
quantity<isq::length[m], safe_int<std::int16_t>> meters = safe_int<std::int16_t>{30'000} * m;
quantity<isq::length[ft], safe_int<std::int16_t>> feet = safe_int<std::int16_t>{10'000} * ft;
// Adding meters + feet works (promoted to safe_int<int>)
auto total = meters + feet;
REQUIRE(total.force_in(m).numerical_value_in(m) > 0);
// But forcing back to int16_t overflows (30000 + ~3048 = ~33048 > 32767)
REQUIRE_THROWS_AS(total.force_in<safe_int<std::int16_t>>(m), std::overflow_error);
}
SECTION("multiplication overflow throws")
{
quantity<isq::length[m], safe_int<int>> dist = safe_int{100'000} * m;
quantity<isq::length[m], safe_int<int>> width = safe_int{100'000} * m;
REQUIRE_THROWS_AS(dist * width, std::overflow_error);
}
SECTION("scalar multiplication overflow throws")
{
quantity<isq::length[m], safe_int<int>> dist = safe_int{std::numeric_limits<int>::max()} * m;
REQUIRE_THROWS_AS(dist * 2, std::overflow_error);
}
SECTION(".in(unit) overflow throws")
{
// 2,200,000 m → µm requires ×1,000,000; product 2.2×10¹² overflows int32_t
quantity<isq::length[m], safe_i32> dist = safe_i32{2'200'000} * m;
REQUIRE_THROWS_AS(dist.in(um), std::overflow_error);
// sanity: a value that fits must not throw
quantity<isq::length[m], safe_i32> small = safe_i32{2} * m;
REQUIRE_NOTHROW(small.in(um));
REQUIRE(small.in(um).numerical_value_in(um) == 2'000'000);
}
SECTION("automatic common-unit scaling overflow throws")
{
// 1,500,000,000 m + 1,000,000 km: converting km→common-unit requires ×10⁶ which overflows int32_t
quantity<isq::length[m], safe_i32> dist_m = safe_i32{1'500'000'000} * m;
quantity<isq::length[km], safe_i32> dist_km = safe_i32{1'000'000} * km;
REQUIRE_THROWS_AS(dist_m + dist_km, std::overflow_error);
// sanity: values that don't overflow must not throw
quantity<isq::length[m], safe_i32> small_m = safe_i32{100} * m;
quantity<isq::length[km], safe_i32> small_km = safe_i32{1} * km;
REQUIRE_NOTHROW(small_m + small_km);
}
SECTION("convenience aliases work")
{
quantity<isq::length[m], safe_i32> dist = safe_i32{100} * m;
REQUIRE(dist.numerical_value_in(m) == 100);
quantity<isq::time[s], safe_i16> time = safe_i16{10} * s;
auto speed = dist / time;
REQUIRE(speed.numerical_value_in(m / s) == 10);
}
SECTION("comparison operations work")
{
quantity<isq::length[m], safe_int<int>> d1 = safe_int{100} * m;
quantity<isq::length[m], safe_int<int>> d2 = safe_int{50} * m;
REQUIRE(d1 > d2);
REQUIRE(d2 < d1);
REQUIRE(d1 >= safe_int{100} * m);
REQUIRE(d2 <= safe_int{100} * m);
REQUIRE(d1 == safe_int{100} * m);
REQUIRE(d1 != d2);
}
}
// ============================================================================
// Scalar op overflow: safe_int<T> op U and U op safe_int<T>
// ============================================================================
TEST_CASE("safe_int * scalar operations", "[safe_int][scalar][throw]")
{
using T = safe_int<int>;
const auto int_max = std::numeric_limits<int>::max();
const auto int_min = std::numeric_limits<int>::min();
SECTION("addition overflow throws")
{
REQUIRE_THROWS_AS(T{int_max} + 1, std::overflow_error);
REQUIRE_THROWS_AS(1 + T{int_max}, std::overflow_error);
REQUIRE_THROWS_AS(T{int_min} + (-1), std::overflow_error);
REQUIRE_THROWS_AS((-1) + T{int_min}, std::overflow_error);
}
SECTION("subtraction overflow throws")
{
REQUIRE_THROWS_AS(T{int_min} - 1, std::overflow_error); // INT_MIN - 1 underflows
REQUIRE_THROWS_AS(1 - T{int_min}, std::overflow_error); // 1 - INT_MIN = INT_MAX+2 overflows
REQUIRE_THROWS_AS(T{int_max} - (-1), std::overflow_error); // INT_MAX - (-1) = INT_MAX+1 overflows
REQUIRE_THROWS_AS((-2) - T{int_max}, std::overflow_error); // -2 - INT_MAX < INT_MIN overflows
}
SECTION("multiplication overflow throws")
{
REQUIRE_THROWS_AS(T{int_max} * 2, std::overflow_error);
REQUIRE_THROWS_AS(2 * T{int_max}, std::overflow_error);
}
SECTION("division overflow throws")
{
REQUIRE_THROWS_AS(T{1} / 0, std::overflow_error); // divide by zero
REQUIRE_THROWS_AS(1 / T{0}, std::overflow_error); // divide by zero
REQUIRE_THROWS_AS(T{int_min} / (-1), std::overflow_error); // INT_MIN / -1
REQUIRE_THROWS_AS(int_min / T{-1}, std::overflow_error); // INT_MIN / -1
}
SECTION("modulo by zero throws")
{
REQUIRE_THROWS_AS(T{10} % 0, std::overflow_error);
REQUIRE_THROWS_AS(10 % T{0}, std::overflow_error);
}
}
// ============================================================================
// Cross-wrapper overflow: constrained<U,CP> op safe_int<T,EP>
// The integral-winner overloads delegate to safe_int ops, so overflow is detected.
// ============================================================================
TEST_CASE("safe_int * constrained operations", "[safe_int][cross-wrapper][throw]")
{
using C = constrained<int>;
using S = safe_int<int>;
const auto int_max = std::numeric_limits<int>::max();
const auto int_min = std::numeric_limits<int>::min();
SECTION("addition overflow throws")
{
REQUIRE_THROWS_AS(C{int_max} + S{1}, std::overflow_error);
REQUIRE_THROWS_AS(S{int_max} + C{1}, std::overflow_error);
}
SECTION("subtraction overflow throws")
{
REQUIRE_THROWS_AS(C{int_min} - S{1}, std::overflow_error);
REQUIRE_THROWS_AS(S{int_min} - C{1}, std::overflow_error);
}
SECTION("multiplication overflow throws")
{
REQUIRE_THROWS_AS(C{int_max} * S{2}, std::overflow_error);
REQUIRE_THROWS_AS(S{int_max} * C{2}, std::overflow_error);
}
SECTION("division overflow throws")
{
REQUIRE_THROWS_AS(C{1} / S{0}, std::overflow_error);
REQUIRE_THROWS_AS(S{1} / C{0}, std::overflow_error);
REQUIRE_THROWS_AS(C{int_min} / S{-1}, std::overflow_error);
REQUIRE_THROWS_AS(S{int_min} / C{-1}, std::overflow_error);
}
SECTION("modulo by zero throws")
{
REQUIRE_THROWS_AS(C{10} % S{0}, std::overflow_error);
REQUIRE_THROWS_AS(S{10} % C{0}, std::overflow_error);
}
}