diff --git a/include/boost/safe_numbers/detail/signed_integer_basis.hpp b/include/boost/safe_numbers/detail/signed_integer_basis.hpp index e985d08..e24cb63 100644 --- a/include/boost/safe_numbers/detail/signed_integer_basis.hpp +++ b/include/boost/safe_numbers/detail/signed_integer_basis.hpp @@ -66,6 +66,9 @@ class signed_integer_basis template constexpr auto operator+=(signed_integer_basis rhs) -> signed_integer_basis&; + + template + constexpr auto operator-=(signed_integer_basis rhs) -> signed_integer_basis&; }; // Helper for diagnostic messages @@ -772,6 +775,353 @@ constexpr auto signed_integer_basis::operator+=(const signed_integer_ return *this; } +// ------------------------------ +// Subtraction +// ------------------------------ + +namespace impl { + +template +constexpr auto signed_overflow_sub_msg() noexcept -> const char* +{ + if constexpr (std::is_same_v) + { + return "Overflow detected in i8 subtraction"; + } + else if constexpr (std::is_same_v) + { + return "Overflow detected in i16 subtraction"; + } + else if constexpr (std::is_same_v) + { + return "Overflow detected in i32 subtraction"; + } + else if constexpr (std::is_same_v) + { + return "Overflow detected in i64 subtraction"; + } + else + { + return "Overflow detected in i128 subtraction"; + } +} + +template +constexpr auto signed_underflow_sub_msg() noexcept -> const char* +{ + if constexpr (std::is_same_v) + { + return "Underflow detected in i8 subtraction"; + } + else if constexpr (std::is_same_v) + { + return "Underflow detected in i16 subtraction"; + } + else if constexpr (std::is_same_v) + { + return "Underflow detected in i32 subtraction"; + } + else if constexpr (std::is_same_v) + { + return "Underflow detected in i64 subtraction"; + } + else + { + return "Underflow detected in i128 subtraction"; + } +} + +#if BOOST_SAFE_NUMBERS_HAS_BUILTIN(__builtin_sub_overflow) + +template +auto signed_intrin_sub(const T lhs, const T rhs, T& result) -> signed_overflow_status +{ + if (__builtin_sub_overflow(lhs, rhs, &result)) + { + return classify_signed_overflow(lhs); + } + + return signed_overflow_status::no_error; +} + +#elif defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X64_INTRIN) || defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X86_INTRIN) + +template +auto signed_intrin_sub(const T lhs, const T rhs, T& result) -> signed_overflow_status +{ + using unsigned_t = std::make_unsigned_t; + + unsigned_t temp {}; + + if constexpr (std::is_same_v) + { + _subborrow_u8(0, static_cast(lhs), static_cast(rhs), &temp); + } + else if constexpr (std::is_same_v) + { + _subborrow_u16(0, static_cast(lhs), static_cast(rhs), &temp); + } + else if constexpr (std::is_same_v) + { + _subborrow_u32(0, static_cast(lhs), static_cast(rhs), &temp); + } + #if defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X86_INTRIN) + else if constexpr (std::is_same_v) + { + // x86 does not provide the _subborrow_u64 intrinsic + temp = static_cast(lhs) - static_cast(rhs); + } + #else + else if constexpr (std::is_same_v) + { + _subborrow_u64(0, static_cast(lhs), static_cast(rhs), &temp); + } + #endif + + result = static_cast(temp); + + // Signed subtraction overflow: operands have different signs and result sign differs from lhs + const auto ulhs {static_cast(lhs)}; + const auto urhs {static_cast(rhs)}; + const auto has_overflow {static_cast(((ulhs ^ urhs) & (ulhs ^ temp)) >> std::numeric_limits::digits)}; + + if (has_overflow) + { + return classify_signed_overflow(lhs); + } + + return signed_overflow_status::no_error; +} + +#endif + +template +constexpr auto signed_no_intrin_sub(const T lhs, const T rhs, T& result) noexcept -> signed_overflow_status +{ + using unsigned_t = make_unsigned_helper_t; + unsigned_t temp {}; + + if constexpr (std::is_same_v || std::is_same_v) + { + temp = static_cast(static_cast(lhs) - static_cast(rhs)); + } + else + { + temp = static_cast(lhs) - static_cast(rhs); + } + + result = static_cast(temp); + + // Signed subtraction overflow: operands have different signs and result sign differs from lhs + const auto ulhs {static_cast(lhs)}; + const auto urhs {static_cast(rhs)}; + const auto has_overflow {static_cast(((ulhs ^ urhs) & (ulhs ^ temp)) >> std::numeric_limits::digits)}; + + if (has_overflow) + { + return classify_signed_overflow(lhs); + } + + return signed_overflow_status::no_error; +} + +template +struct signed_sub_helper +{ + [[nodiscard]] static constexpr auto apply(const signed_integer_basis lhs, + const signed_integer_basis rhs) + noexcept(Policy != overflow_policy::throw_exception) + -> signed_integer_basis + { + using result_type = signed_integer_basis; + + const auto lhs_basis {static_cast(lhs)}; + const auto rhs_basis {static_cast(rhs)}; + BasisType result {}; + + auto handle_error = [&result](signed_overflow_status status) + { + if (std::is_constant_evaluated()) + { + if (status == signed_overflow_status::overflow) + { + if constexpr (std::is_same_v) + { + throw std::overflow_error("Overflow detected in i8 subtraction"); + } + else if constexpr (std::is_same_v) + { + throw std::overflow_error("Overflow detected in i16 subtraction"); + } + else if constexpr (std::is_same_v) + { + throw std::overflow_error("Overflow detected in i32 subtraction"); + } + else if constexpr (std::is_same_v) + { + throw std::overflow_error("Overflow detected in i64 subtraction"); + } + else + { + throw std::overflow_error("Overflow detected in i128 subtraction"); + } + } + else + { + if constexpr (std::is_same_v) + { + throw std::underflow_error("Underflow detected in i8 subtraction"); + } + else if constexpr (std::is_same_v) + { + throw std::underflow_error("Underflow detected in i16 subtraction"); + } + else if constexpr (std::is_same_v) + { + throw std::underflow_error("Underflow detected in i32 subtraction"); + } + else if constexpr (std::is_same_v) + { + throw std::underflow_error("Underflow detected in i64 subtraction"); + } + else + { + throw std::underflow_error("Underflow detected in i128 subtraction"); + } + } + } + else + { + if constexpr (Policy == overflow_policy::throw_exception) + { + static_cast(result); + + if (status == signed_overflow_status::overflow) + { + BOOST_SAFE_NUMBERS_THROW_EXCEPTION(std::overflow_error, signed_overflow_sub_msg()); + } + else + { + BOOST_SAFE_NUMBERS_THROW_EXCEPTION(std::underflow_error, signed_underflow_sub_msg()); + } + } + else + { + static_cast(result); + BOOST_SAFE_NUMBERS_UNREACHABLE; + } + } + }; + + #if BOOST_SAFE_NUMBERS_HAS_BUILTIN(__builtin_sub_overflow) || defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X64_INTRIN) || defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X86_INTRIN) + + if constexpr (!std::is_same_v) + { + if (!std::is_constant_evaluated()) + { + const auto status {impl::signed_intrin_sub(lhs_basis, rhs_basis, result)}; + if (status != signed_overflow_status::no_error) + { + handle_error(status); + } + + return result_type{result}; + } + } + + #endif // BOOST_SAFE_NUMBERS_HAS_BUILTIN(__builtin_sub_overflow) || defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X64_INTRIN) || defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X86_INTRIN) + + const auto status {impl::signed_no_intrin_sub(lhs_basis, rhs_basis, result)}; + if (status != signed_overflow_status::no_error) + { + handle_error(status); + } + + return result_type{result}; + } +}; + +template +[[nodiscard]] constexpr auto sub_impl(const signed_integer_basis lhs, + const signed_integer_basis rhs) +{ + return signed_sub_helper::apply(lhs, rhs); +} + +} // namespace impl + +template +[[nodiscard]] constexpr auto operator-(const signed_integer_basis lhs, + const signed_integer_basis rhs) -> signed_integer_basis +{ + if (std::is_constant_evaluated()) + { + BasisType res {}; + const auto status {impl::signed_no_intrin_sub(static_cast(lhs), static_cast(rhs), res)}; + if (status == impl::signed_overflow_status::overflow) + { + if constexpr (std::is_same_v) + { + throw std::overflow_error("Overflow detected in i8 subtraction"); + } + else if constexpr (std::is_same_v) + { + throw std::overflow_error("Overflow detected in i16 subtraction"); + } + else if constexpr (std::is_same_v) + { + throw std::overflow_error("Overflow detected in i32 subtraction"); + } + else if constexpr (std::is_same_v) + { + throw std::overflow_error("Overflow detected in i64 subtraction"); + } + else + { + throw std::overflow_error("Overflow detected in i128 subtraction"); + } + } + else if (status == impl::signed_overflow_status::underflow) + { + if constexpr (std::is_same_v) + { + throw std::underflow_error("Underflow detected in i8 subtraction"); + } + else if constexpr (std::is_same_v) + { + throw std::underflow_error("Underflow detected in i16 subtraction"); + } + else if constexpr (std::is_same_v) + { + throw std::underflow_error("Underflow detected in i32 subtraction"); + } + else if constexpr (std::is_same_v) + { + throw std::underflow_error("Underflow detected in i64 subtraction"); + } + else + { + throw std::underflow_error("Underflow detected in i128 subtraction"); + } + } + + return signed_integer_basis{res}; + } + + return impl::signed_sub_helper::apply(lhs, rhs); +} + +BOOST_SAFE_NUMBERS_DEFINE_MIXED_SIGNED_INTEGER_OP("subtraction", operator-) + +template +template +constexpr auto signed_integer_basis::operator-=(const signed_integer_basis rhs) + -> signed_integer_basis& +{ + *this = *this - rhs; + return *this; +} + } // namespace boost::safe_numbers::detail #undef BOOST_SAFE_NUMBERS_DEFINE_MIXED_SIGNED_INTEGER_OP diff --git a/test/Jamfile b/test/Jamfile index 0a25788..e9b9f33 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -57,6 +57,7 @@ run test_signed_conversions.cpp ; run test_signed_comparisons.cpp ; run test_signed_unary_operators.cpp ; run test_signed_addition.cpp ; +run test_signed_subtraction.cpp ; run test_unsigned_addition.cpp ; compile-fail compile_fail_unsigned_addition.cpp ; diff --git a/test/test_signed_subtraction.cpp b/test/test_signed_subtraction.cpp new file mode 100644 index 0000000..6ec1207 --- /dev/null +++ b/test/test_signed_subtraction.cpp @@ -0,0 +1,354 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wold-style-cast" +# pragma clang diagnostic ignored "-Wundef" +# pragma clang diagnostic ignored "-Wconversion" +# pragma clang diagnostic ignored "-Wsign-conversion" +# pragma clang diagnostic ignored "-Wfloat-equal" +# pragma clang diagnostic ignored "-Wsign-compare" +# pragma clang diagnostic ignored "-Woverflow" + +# if (__clang_major__ >= 10 && !defined(__APPLE__)) || __clang_major__ >= 13 +# pragma clang diagnostic ignored "-Wdeprecated-copy" +# endif + +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wold-style-cast" +# pragma GCC diagnostic ignored "-Wundef" +# pragma GCC diagnostic ignored "-Wconversion" +# pragma GCC diagnostic ignored "-Wsign-conversion" +# pragma GCC diagnostic ignored "-Wsign-compare" +# pragma GCC diagnostic ignored "-Wfloat-equal" +# pragma GCC diagnostic ignored "-Woverflow" + +#elif defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 4389) +# pragma warning(disable : 4127) +# pragma warning(disable : 4305) +# pragma warning(disable : 4309) +#endif + +#define BOOST_SAFE_NUMBERS_DETAIL_INT128_ALLOW_SIGN_COMPARE +#define BOOST_SAFE_NUMBERS_DETAIL_INT128_ALLOW_SIGN_CONVERSION + +#include + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#elif defined(_MSC_VER) +# pragma warning(pop) +#endif + +// Ignore [[nodiscard]] on the tests that we know are going to throw +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-result" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-result" +#elif defined(_MSC_VER) +# pragma warning (push) +# pragma warning (disable: 4834) +#endif + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include +#include +#include +#include + +#endif + +using namespace boost::safe_numbers; + +inline std::mt19937_64 rng{42}; +inline constexpr std::size_t N {1024}; + +// ----------------------------------------------- +// Valid subtraction: values in the safe half-range +// ----------------------------------------------- + +template +void test_valid_subtraction() +{ + using basis_type = detail::underlying_type_t; + + // Use quarter-range to ensure no overflow when subtracting two values + boost::random::uniform_int_distribution dist {static_cast(std::numeric_limits::min() / 2), + static_cast(std::numeric_limits::max() / 2)}; + + for (std::size_t i {0}; i < N; ++i) + { + const auto lhs_value {dist(rng)}; + const auto rhs_value {dist(rng)}; + + // For int8_t/int16_t, promotion to int means no overflow in the builtin subtraction + T ref_value {}; + if constexpr (std::is_same_v || std::is_same_v) + { + ref_value = T{static_cast(static_cast(lhs_value) - static_cast(rhs_value))}; + } + else + { + ref_value = T{static_cast(lhs_value - rhs_value)}; + } + + const T lhs {lhs_value}; + const T rhs {rhs_value}; + const auto res {lhs - rhs}; + + BOOST_TEST(ref_value == res); + } +} + +// ----------------------------------------------- +// Positive overflow: positive - negative near min +// e.g. near_max - (-2) overflows +// ----------------------------------------------- + +template +void test_positive_overflow() +{ + using basis_type = detail::underlying_type_t; + boost::random::uniform_int_distribution dist {std::numeric_limits::min(), + basis_type{-2}}; + + for (std::size_t i {0}; i < N; ++i) + { + constexpr auto lhs_value {static_cast(std::numeric_limits::max() - 1)}; + const auto rhs_value {dist(rng)}; + + const T lhs {lhs_value}; + const T rhs {rhs_value}; + + BOOST_TEST_THROWS(lhs - rhs, std::overflow_error); + } +} + +// ----------------------------------------------- +// Negative overflow (underflow): negative - positive +// e.g. near_min - 2 underflows +// ----------------------------------------------- + +template +void test_negative_overflow() +{ + using basis_type = detail::underlying_type_t; + boost::random::uniform_int_distribution dist {basis_type{2}, + std::numeric_limits::max()}; + + for (std::size_t i {0}; i < N; ++i) + { + constexpr auto lhs_value {static_cast(std::numeric_limits::min() + 1)}; + const auto rhs_value {dist(rng)}; + + const T lhs {lhs_value}; + const T rhs {rhs_value}; + + BOOST_TEST_THROWS(lhs - rhs, std::underflow_error); + } +} + +// ----------------------------------------------- +// Same-sign subtraction: should never overflow +// Positive - positive and negative - negative can never exceed the range +// ----------------------------------------------- + +template +void test_same_sign_subtraction() +{ + using basis_type = detail::underlying_type_t; + boost::random::uniform_int_distribution pos_dist {basis_type{0}, + std::numeric_limits::max()}; + boost::random::uniform_int_distribution neg_dist {std::numeric_limits::min(), + basis_type{-1}}; + + for (std::size_t i {0}; i < N; ++i) + { + // positive - positive + { + const auto lhs_value {pos_dist(rng)}; + const auto rhs_value {pos_dist(rng)}; + + const T lhs {lhs_value}; + const T rhs {rhs_value}; + + const auto res {lhs - rhs}; + + T expected {}; + if constexpr (std::is_same_v || std::is_same_v) + { + expected = T{static_cast(static_cast(lhs_value) - static_cast(rhs_value))}; + } + else + { + expected = T{static_cast(lhs_value - rhs_value)}; + } + + BOOST_TEST(res == expected); + } + + // negative - negative + { + const auto lhs_value {neg_dist(rng)}; + const auto rhs_value {neg_dist(rng)}; + + const T lhs {lhs_value}; + const T rhs {rhs_value}; + + const auto res {lhs - rhs}; + + T expected {}; + if constexpr (std::is_same_v || std::is_same_v) + { + expected = T{static_cast(static_cast(lhs_value) - static_cast(rhs_value))}; + } + else + { + expected = T{static_cast(lhs_value - rhs_value)}; + } + + BOOST_TEST(res == expected); + } + } +} + +// ----------------------------------------------- +// Compound assignment: operator-= +// ----------------------------------------------- + +template +void test_minus_equals() +{ + using basis_type = detail::underlying_type_t; + + // Use quarter-range to ensure no overflow + boost::random::uniform_int_distribution dist {static_cast(std::numeric_limits::min() / 2), + static_cast(std::numeric_limits::max() / 2)}; + + for (std::size_t i {0}; i < N; ++i) + { + const auto lhs_value {dist(rng)}; + const auto rhs_value {dist(rng)}; + + const T lhs {lhs_value}; + const T rhs {rhs_value}; + + // operator- result for reference + const auto expected {lhs - rhs}; + + // operator-= should give the same result + T result {lhs_value}; + result -= rhs; + BOOST_TEST(result == expected); + } + + // -= with overflow should throw (positive - negative) + T near_max {static_cast(std::numeric_limits::max() - 1)}; + BOOST_TEST_THROWS(near_max -= T{basis_type{-2}}, std::overflow_error); + + // -= with underflow should throw (negative - positive) + T near_min {static_cast(std::numeric_limits::min() + 1)}; + BOOST_TEST_THROWS(near_min -= T{basis_type{2}}, std::underflow_error); + + // -= with zero is identity + T val {basis_type{42}}; + val -= T{basis_type{0}}; + BOOST_TEST(val == T{basis_type{42}}); +} + +// ----------------------------------------------- +// Specific boundary cases +// ----------------------------------------------- + +template +void test_boundary_cases() +{ + using basis_type = detail::underlying_type_t; + + // 0 - 0 = 0 + BOOST_TEST(T{basis_type{0}} - T{basis_type{0}} == T{basis_type{0}}); + + // 1 - 1 = 0 + BOOST_TEST(T{basis_type{1}} - T{basis_type{1}} == T{basis_type{0}}); + + // 0 - 1 = -1 + BOOST_TEST(T{basis_type{0}} - T{basis_type{1}} == T{basis_type{-1}}); + + // max - 0 = max + BOOST_TEST(T{std::numeric_limits::max()} - T{basis_type{0}} == T{std::numeric_limits::max()}); + + // min - 0 = min + BOOST_TEST(T{std::numeric_limits::min()} - T{basis_type{0}} == T{std::numeric_limits::min()}); + + // max - max = 0 + BOOST_TEST(T{std::numeric_limits::max()} - T{std::numeric_limits::max()} == T{basis_type{0}}); + + // min - min = 0 + BOOST_TEST(T{std::numeric_limits::min()} - T{std::numeric_limits::min()} == T{basis_type{0}}); + + // max - (-1) overflows + BOOST_TEST_THROWS(T{std::numeric_limits::max()} - T{basis_type{-1}}, std::overflow_error); + + // min - 1 underflows + BOOST_TEST_THROWS(T{std::numeric_limits::min()} - T{basis_type{1}}, std::underflow_error); + + // 0 - min overflows (since -min > max for two's complement) + BOOST_TEST_THROWS(T{basis_type{0}} - T{std::numeric_limits::min()}, std::overflow_error); +} + +int main() +{ + test_valid_subtraction(); + test_valid_subtraction(); + test_valid_subtraction(); + test_valid_subtraction(); + test_valid_subtraction(); + + test_positive_overflow(); + test_positive_overflow(); + test_positive_overflow(); + test_positive_overflow(); + test_positive_overflow(); + + test_negative_overflow(); + test_negative_overflow(); + test_negative_overflow(); + test_negative_overflow(); + test_negative_overflow(); + + test_same_sign_subtraction(); + test_same_sign_subtraction(); + test_same_sign_subtraction(); + test_same_sign_subtraction(); + test_same_sign_subtraction(); + + test_minus_equals(); + test_minus_equals(); + test_minus_equals(); + test_minus_equals(); + test_minus_equals(); + + test_boundary_cases(); + test_boundary_cases(); + test_boundary_cases(); + test_boundary_cases(); + test_boundary_cases(); + + return boost::report_errors(); +}