diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 820b50b..4a4a9ad 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -25,6 +25,7 @@ ** xref:api_reference.adoc#api_headers[Headers] * xref:policies.adoc[] * xref:unsigned_integers.adoc[] +* xref:signed_integers.adoc[] * xref:bounded_uint.adoc[] * xref:cuda.adoc[] * xref:literals.adoc[] diff --git a/doc/modules/ROOT/pages/api_reference.adoc b/doc/modules/ROOT/pages/api_reference.adoc index 174f204..3965107 100644 --- a/doc/modules/ROOT/pages/api_reference.adoc +++ b/doc/modules/ROOT/pages/api_reference.adoc @@ -43,6 +43,28 @@ https://www.boost.org/LICENSE_1_0.txt | Safe unsigned 128-bit integer |=== +=== Signed Integer Types + +[cols="1,2", options="header"] +|=== +| Type | Description + +| xref:signed_integers.adoc[`i8`] +| Safe signed 8-bit integer + +| xref:signed_integers.adoc[`i16`] +| Safe signed 16-bit integer + +| xref:signed_integers.adoc[`i32`] +| Safe signed 32-bit integer + +| xref:signed_integers.adoc[`i64`] +| Safe signed 64-bit integer + +| xref:signed_integers.adoc[`i128`] +| Safe signed 128-bit integer +|=== + === Bounded Unsigned Integer Type [cols="1,2", options="header"] @@ -351,6 +373,9 @@ This header is not included in the convenience header since it requires external | `` | The `overflow_policy` enum class +| `` +| All signed safe integer types (`i8`, `i16`, `i32`, `i64`, `i128`) + | `` | All unsigned safe integer types (`u8`, `u16`, `u32`, `u64`, `u128`) diff --git a/doc/modules/ROOT/pages/signed_integers.adoc b/doc/modules/ROOT/pages/signed_integers.adoc new file mode 100644 index 0000000..f3e2998 --- /dev/null +++ b/doc/modules/ROOT/pages/signed_integers.adoc @@ -0,0 +1,150 @@ +//// +Copyright 2026 Matt Borland +Distributed under the Boost Software License, Version 1.0. +https://www.boost.org/LICENSE_1_0.txt +//// + +[#signed_integers] + += Signed Integer Types + +== Description + +The library provides safe signed integer types that detect overflow, underflow, division by zero, and other undefined behavior at runtime. +These types are drop-in replacements for the standard signed integer types with added safety guarantees. + +|=== +| Type | Underlying Type | Width | Min | Max + +| `i8` | `std::int8_t` | 8 bits | -128 | 127 +| `i16` | `std::int16_t` | 16 bits | -32,768 | 32,767 +| `i32` | `std::int32_t` | 32 bits | -2,147,483,648 | 2,147,483,647 +| `i64` | `std::int64_t` | 64 bits | -9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 +| `i128` | `int128_t` | 128 bits | -170,141,183,460,469,231,731,687,303,715,884,105,728 | 170,141,183,460,469,231,731,687,303,715,884,105,727 +|=== + +Each type exposes a `basis_type` member type alias that refers to the underlying integer type, allowing conversion back to built-in types when needed. + +[source,c++] +---- +#include + +namespace boost::safe_numbers { + +using i8 = detail::signed_integer_basis; +using i16 = detail::signed_integer_basis; +using i32 = detail::signed_integer_basis; +using i64 = detail::signed_integer_basis; +using i128 = detail::signed_integer_basis; + +template +class signed_integer_basis { + +public: + using basis_type = BasisType; + + // Construction + constexpr signed_integer_basis() noexcept = default; + explicit constexpr signed_integer_basis(BasisType val) noexcept; + + template + requires std::is_same_v + explicit constexpr signed_integer_basis(T) noexcept = delete; // bool prohibited + + // Conversion to underlying types + template + explicit constexpr operator OtherBasis() const; + + // Comparison operators + friend constexpr auto operator<=>(signed_integer_basis lhs, signed_integer_basis rhs) noexcept + -> std::strong_ordering = default; + + // Unary operators + constexpr auto operator+() const noexcept -> signed_integer_basis; + constexpr auto operator-() const -> signed_integer_basis; + + // Compound assignment operators (arithmetic) + template + constexpr auto operator+=(signed_integer_basis rhs) -> 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&; + + template + constexpr auto operator/=(signed_integer_basis rhs) -> signed_integer_basis&; + + template + constexpr auto operator%=(signed_integer_basis rhs) -> signed_integer_basis&; + + // Increment and decrement operators + constexpr auto operator++() -> signed_integer_basis&; + constexpr auto operator++(int) -> signed_integer_basis; + constexpr auto operator--() -> signed_integer_basis&; + constexpr auto operator--(int) -> signed_integer_basis; + +}; // class signed_integer_basis + +// Arithmetic operators (throw on overflow/underflow) +template +constexpr auto operator+(signed_integer_basis lhs, + signed_integer_basis rhs) -> signed_integer_basis; + +template +constexpr auto operator-(signed_integer_basis lhs, + signed_integer_basis rhs) -> signed_integer_basis; + +template +constexpr auto operator*(signed_integer_basis lhs, + signed_integer_basis rhs) -> signed_integer_basis; + +template +constexpr auto operator/(signed_integer_basis lhs, + signed_integer_basis rhs) -> signed_integer_basis; + +template +constexpr auto operator%(signed_integer_basis lhs, + signed_integer_basis rhs) -> signed_integer_basis; + +} // namespace boost::safe_numbers +---- + +== Increment and Decrement Operators + +[source,c++] +---- +constexpr auto operator++() -> signed_integer_basis&; +constexpr auto operator++(int) -> signed_integer_basis; +constexpr auto operator--() -> signed_integer_basis&; +constexpr auto operator--(int) -> signed_integer_basis; +---- + +- `++` (pre/post): Throws `std::overflow_error` if the value is already at `std::numeric_limits::max()` +- `--` (pre/post): Throws `std::underflow_error` if the value is already at `std::numeric_limits::min()` + +== Unary Operators + +[source,c++] +---- +constexpr auto operator+() const noexcept -> signed_integer_basis; +constexpr auto operator-() const -> signed_integer_basis; +---- + +- `+`: Returns a copy of the value (identity). +- `-`: Throws `std::domain_error` if the value is `std::numeric_limits::min()`, since negating the minimum value of a two's complement signed integer overflows. + +== Mixed-Width Operations + +Operations between different width safe signed integer types are compile-time errors. +The operands must be promoted to a common type explicitly before performing the operation. + +[source,c++] +---- +auto a = i16{100}; +auto b = i32{200}; + +// auto result = a + b; // Compile error: mismatched types +auto result = static_cast(a) + b; // OK: explicit promotion +---- diff --git a/include/boost/safe_numbers/detail/signed_integer_basis.hpp b/include/boost/safe_numbers/detail/signed_integer_basis.hpp index a51c6bd..e42b51a 100644 --- a/include/boost/safe_numbers/detail/signed_integer_basis.hpp +++ b/include/boost/safe_numbers/detail/signed_integer_basis.hpp @@ -78,6 +78,14 @@ class signed_integer_basis template constexpr auto operator%=(signed_integer_basis rhs) -> signed_integer_basis&; + + constexpr auto operator++() -> signed_integer_basis&; + + constexpr auto operator++(int) -> signed_integer_basis; + + constexpr auto operator--() -> signed_integer_basis&; + + constexpr auto operator--(int) -> signed_integer_basis; }; // Helper for diagnostic messages @@ -1947,6 +1955,122 @@ constexpr auto signed_integer_basis::operator%=(const signed_integer_ return *this; } +// ------------------------------ +// Increment / Decrement error messages +// ------------------------------ + +template +constexpr auto signed_overflow_inc_msg() noexcept -> const char* +{ + if constexpr (std::is_same_v) + { + return "Overflow detected in i8 increment"; + } + else if constexpr (std::is_same_v) + { + return "Overflow detected in i16 increment"; + } + else if constexpr (std::is_same_v) + { + return "Overflow detected in i32 increment"; + } + else if constexpr (std::is_same_v) + { + return "Overflow detected in i64 increment"; + } + else + { + return "Overflow detected in i128 increment"; + } +} + +template +constexpr auto signed_underflow_dec_msg() noexcept -> const char* +{ + if constexpr (std::is_same_v) + { + return "Underflow detected in i8 decrement"; + } + else if constexpr (std::is_same_v) + { + return "Underflow detected in i16 decrement"; + } + else if constexpr (std::is_same_v) + { + return "Underflow detected in i32 decrement"; + } + else if constexpr (std::is_same_v) + { + return "Underflow detected in i64 decrement"; + } + else + { + return "Underflow detected in i128 decrement"; + } +} + +// ------------------------------ +// Pre and post increment +// ------------------------------ + +template +constexpr auto signed_integer_basis::operator++() + -> signed_integer_basis& +{ + if (this->basis_ == std::numeric_limits::max()) [[unlikely]] + { + BOOST_SAFE_NUMBERS_THROW_EXCEPTION(std::overflow_error, signed_overflow_inc_msg()); + } + + ++this->basis_; + return *this; +} + +template +constexpr auto signed_integer_basis::operator++(int) + -> signed_integer_basis +{ + if (this->basis_ == std::numeric_limits::max()) [[unlikely]] + { + BOOST_SAFE_NUMBERS_THROW_EXCEPTION(std::overflow_error, signed_overflow_inc_msg()); + } + + const auto temp {*this}; + ++this->basis_; + return temp; +} + +// ------------------------------ +// Pre and post decrement +// ------------------------------ + +template +constexpr auto signed_integer_basis::operator--() + -> signed_integer_basis& +{ + if (this->basis_ == std::numeric_limits::min()) [[unlikely]] + { + BOOST_SAFE_NUMBERS_THROW_EXCEPTION(std::underflow_error, signed_underflow_dec_msg()); + } + + --this->basis_; + return *this; +} + +template +constexpr auto signed_integer_basis::operator--(int) + -> signed_integer_basis +{ + if (this->basis_ == std::numeric_limits::min()) [[unlikely]] + { + BOOST_SAFE_NUMBERS_THROW_EXCEPTION(std::underflow_error, signed_underflow_dec_msg()); + } + + const auto temp {*this}; + --this->basis_; + return temp; +} + } // namespace boost::safe_numbers::detail #undef BOOST_SAFE_NUMBERS_DEFINE_MIXED_SIGNED_INTEGER_OP diff --git a/test/Jamfile b/test/Jamfile index b481172..0e34cbc 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -61,6 +61,7 @@ run test_signed_subtraction.cpp ; run test_signed_multiplication.cpp ; run test_signed_division.cpp ; run test_signed_modulo.cpp ; +run test_signed_inc_dec.cpp ; run test_unsigned_addition.cpp ; compile-fail compile_fail_unsigned_addition.cpp ; diff --git a/test/test_signed_inc_dec.cpp b/test/test_signed_inc_dec.cpp new file mode 100644 index 0000000..17b85dc --- /dev/null +++ b/test/test_signed_inc_dec.cpp @@ -0,0 +1,137 @@ +// Copyright 2025 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include +#include + +#endif + +#include + +using namespace boost::safe_numbers; + +template +void test_increment() +{ + using basis = typename T::basis_type; + + basis builtin {0}; + T safe_type {builtin}; + + for (std::size_t i {}; i < 20; ++i) + { + const auto new_builtin {++builtin}; + const auto new_safe {++safe_type}; + + BOOST_TEST_EQ(new_builtin, static_cast(new_safe)); + } + + for (std::size_t i {}; i < 20; ++i) + { + const auto new_builtin {builtin++}; + const auto new_safe {safe_type++}; + + BOOST_TEST_EQ(new_builtin, static_cast(new_safe)); + } + + BOOST_TEST_THROWS(++T{std::numeric_limits::max()}, std::overflow_error); + BOOST_TEST_THROWS(T{std::numeric_limits::max()}++, std::overflow_error); +} + +template +void test_decrement() +{ + using basis = typename T::basis_type; + + basis builtin {100}; + T safe_type {builtin}; + + for (std::size_t i {}; i < 20; ++i) + { + const auto new_builtin {--builtin}; + const auto new_safe {--safe_type}; + + BOOST_TEST_EQ(new_builtin, static_cast(new_safe)); + } + + for (std::size_t i {}; i < 20; ++i) + { + const auto new_builtin {builtin--}; + const auto new_safe {safe_type--}; + + BOOST_TEST_EQ(new_builtin, static_cast(new_safe)); + } + + BOOST_TEST_THROWS(--T{std::numeric_limits::min()}, std::underflow_error); + BOOST_TEST_THROWS(T{std::numeric_limits::min()}--, std::underflow_error); +} + +template +void test_negative_increment() +{ + using basis = typename T::basis_type; + + basis builtin {-10}; + T safe_type {builtin}; + + for (std::size_t i {}; i < 20; ++i) + { + const auto new_builtin {++builtin}; + const auto new_safe {++safe_type}; + + BOOST_TEST_EQ(new_builtin, static_cast(new_safe)); + } +} + +template +void test_negative_decrement() +{ + using basis = typename T::basis_type; + + basis builtin {10}; + T safe_type {builtin}; + + for (std::size_t i {}; i < 20; ++i) + { + const auto new_builtin {--builtin}; + const auto new_safe {--safe_type}; + + BOOST_TEST_EQ(new_builtin, static_cast(new_safe)); + } +} + +int main() +{ + test_increment(); + test_increment(); + test_increment(); + test_increment(); + test_increment(); + + test_decrement(); + test_decrement(); + test_decrement(); + test_decrement(); + test_decrement(); + + test_negative_increment(); + test_negative_increment(); + test_negative_increment(); + test_negative_increment(); + test_negative_increment(); + + test_negative_decrement(); + test_negative_decrement(); + test_negative_decrement(); + test_negative_decrement(); + test_negative_decrement(); + + return boost::report_errors(); +}