diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 6525ad5..abdcbac 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -19,6 +19,7 @@ ** xref:api_reference.adoc#api_headers[Headers] * xref:policies.adoc[] * xref:unsigned_integers.adoc[] +* xref:bounded_uint.adoc[] * xref:literals.adoc[] * xref:limits.adoc[] * xref:format.adoc[] diff --git a/doc/modules/ROOT/pages/api_reference.adoc b/doc/modules/ROOT/pages/api_reference.adoc index 815aebd..5d4ed0b 100644 --- a/doc/modules/ROOT/pages/api_reference.adoc +++ b/doc/modules/ROOT/pages/api_reference.adoc @@ -33,6 +33,16 @@ https://www.boost.org/LICENSE_1_0.txt | Safe unsigned 128-bit integer |=== +=== Bounded Unsigned Integer Type + +[cols="1,2", options="header"] +|=== +| Type | Description + +| xref:bounded_uint.adoc[`bounded_uint`] +| Safe unsigned integer constrained to a compile-time range `[Min, Max]` +|=== + === Enumerations [cols="1,2", options="header"] @@ -184,4 +194,7 @@ This header is not included in the convenience header since it requires external | `` | All unsigned safe integer types (`u8`, `u16`, `u32`, `u64`, `u128`) + +| `` +| Bounded unsigned integer type (`bounded_uint`) |=== diff --git a/doc/modules/ROOT/pages/bit.adoc b/doc/modules/ROOT/pages/bit.adoc index 8f4d26f..95fc696 100644 --- a/doc/modules/ROOT/pages/bit.adoc +++ b/doc/modules/ROOT/pages/bit.adoc @@ -10,7 +10,7 @@ https://www.boost.org/LICENSE_1_0.txt == Description -The library provides thin wrappers around the C++20 https://en.cppreference.com/w/cpp/header/bit.html[``] functions for safe integer types. +The library provides thin wrappers around the C++20 https://en.cppreference.com/w/cpp/header/bit.html[``] functions for all safe integer types, including `bounded_uint`. Each function extracts the underlying value, delegates to the corresponding `std::` function, and wraps the result back into the safe type where appropriate. For `u128`, the functions delegate to the `boost::int128` implementations. @@ -73,24 +73,28 @@ See https://en.cppreference.com/w/cpp/numeric/bit_width.html[`std::bit_width`]. [source,c++] ---- -template +template constexpr auto rotl(UnsignedInt x, int s) noexcept -> UnsignedInt; ---- Computes the result of bitwise left-rotating `x` by `s` positions. See https://en.cppreference.com/w/cpp/numeric/rotl.html[`std::rotl`]. +NOTE: `rotl` is not available for `bounded_uint` types. Bit rotation can produce values outside the valid bounded range, making the operation semantically meaningless for bounded integers. + === rotr [source,c++] ---- -template +template constexpr auto rotr(UnsignedInt x, int s) noexcept -> UnsignedInt; ---- Computes the result of bitwise right-rotating `x` by `s` positions. See https://en.cppreference.com/w/cpp/numeric/rotr.html[`std::rotr`]. +NOTE: `rotr` is not available for `bounded_uint` types. Bit rotation can produce values outside the valid bounded range, making the operation semantically meaningless for bounded integers. + == Counting Functions === countl_zero @@ -154,7 +158,7 @@ See https://en.cppreference.com/w/cpp/numeric/popcount.html[`std::popcount`]. [source,c++] ---- -template +template constexpr auto byteswap(UnsignedInt x) noexcept -> UnsignedInt; ---- @@ -163,6 +167,8 @@ For standard unsigned types this delegates to `std::byteswap` (C++23). For `u128` this delegates to the `boost::int128::byteswap` implementation. See https://en.cppreference.com/w/cpp/numeric/byteswap.html[`std::byteswap`]. +NOTE: `byteswap` is not available for `bounded_uint` types. Byte reversal can produce values outside the valid bounded range, making the operation semantically meaningless for bounded integers. + == Examples .This https://github.com/boostorg/safe_numbers/blob/develop/examples/bit.cpp[example] demonstrates the bit manipulation functions. @@ -190,5 +196,13 @@ countr_one(0x0F00) = 0 popcount(0x0F00) = 4 byteswap(0x12345678) = 0x78563412 + +bounded has_single_bit(40) = 0 +bounded bit_ceil(40) = 64 +bounded bit_floor(40) = 32 +bounded bit_width(40) = 6 +bounded popcount(40) = 2 +bounded countl_zero(0x0F00) = 4 +bounded popcount(0x0F00) = 4 ---- ==== diff --git a/doc/modules/ROOT/pages/bounded_uint.adoc b/doc/modules/ROOT/pages/bounded_uint.adoc new file mode 100644 index 0000000..8ec3560 --- /dev/null +++ b/doc/modules/ROOT/pages/bounded_uint.adoc @@ -0,0 +1,213 @@ +//// +Copyright 2026 Matt Borland +Distributed under the Boost Software License, Version 1.0. +https://www.boost.org/LICENSE_1_0.txt +//// + +[#bounded_uint] + += Bounded Unsigned Integer Type + +== Description + +The `bounded_uint` class template provides a safe unsigned integer type whose values are constrained to a compile-time range `[Min, Max]`. +Any operation that would produce a value outside this range throws an exception at runtime. + +The underlying storage type (`basis_type`) is automatically selected as the smallest safe unsigned integer type capable of representing `Max`: + +|=== +| Max Value Range | Selected `basis_type` + +| 0 -- 255 | `u8` +| 256 -- 65,535 | `u16` +| 65,536 -- 4,294,967,295 | `u32` +| 4,294,967,296 -- 2^64 - 1 | `u64` +| 2^64 -- 2^128 - 1 | `u128` +|=== + +The template parameters `Min` and `Max` can be any non-`bool` unsigned type, including the library's own safe unsigned types (`u8`, `u16`, etc.) or built-in unsigned types (`std::uint8_t`, `unsigned int`, etc.). +`Max` must be strictly greater than `Min`. + +[source,c++] +---- +#include + +namespace boost::safe_numbers { + +template + requires (detail::valid_bound && + detail::valid_bound && + detail::raw_value(Max) > detail::raw_value(Min)) +class bounded_uint +{ +public: + using basis_type = /* smallest safe unsigned type that fits Max */; + + // Construction + explicit constexpr bounded_uint(basis_type val); + + // Conversion to underlying types + template + explicit constexpr operator OtherBasis() const; + + // Conversion to other bounded_uint types + template + explicit constexpr operator bounded_uint() const; + + // Comparison operators + friend constexpr auto operator<=>(bounded_uint lhs, bounded_uint rhs) noexcept + -> std::strong_ordering = default; + + // Compound assignment operators + constexpr auto operator+=(bounded_uint rhs) -> bounded_uint&; + constexpr auto operator-=(bounded_uint rhs) -> bounded_uint&; + constexpr auto operator*=(bounded_uint rhs) -> bounded_uint&; + constexpr auto operator/=(bounded_uint rhs) -> bounded_uint&; + + // Increment and decrement operators + constexpr auto operator++() -> bounded_uint&; + constexpr auto operator++(int) -> bounded_uint; + constexpr auto operator--() -> bounded_uint&; + constexpr auto operator--(int) -> bounded_uint; +}; + +// Arithmetic operators (throw on out-of-bounds result) +template +constexpr auto operator+(bounded_uint lhs, + bounded_uint rhs) -> bounded_uint; + +template +constexpr auto operator-(bounded_uint lhs, + bounded_uint rhs) -> bounded_uint; + +template +constexpr auto operator*(bounded_uint lhs, + bounded_uint rhs) -> bounded_uint; + +template +constexpr auto operator/(bounded_uint lhs, + bounded_uint rhs) -> bounded_uint; + +template +constexpr auto operator%(bounded_uint lhs, + bounded_uint rhs) -> bounded_uint; + +} // namespace boost::safe_numbers +---- + +== Operator Behavior + +=== Construction + +[source,c++] +---- +explicit constexpr bounded_uint(basis_type val); +---- + +Constructs a `bounded_uint` from a value of the underlying `basis_type`. +The value is default-initialized to `Min`. +If `val` is less than `Min` or greater than `Max`, `std::domain_error` is thrown. + +=== Conversion to Underlying Types + +[source,c++] +---- +template +explicit constexpr operator OtherBasis() const; +---- + +Conversion to other unsigned integral types is explicit. +Widening conversions always succeed. +Narrowing conversions perform a runtime bounds check: if the value exceeds the maximum representable value of the target type, `std::domain_error` is thrown. + +=== Conversion to Other Bounded Types + +[source,c++] +---- +template +explicit constexpr operator bounded_uint() const; +---- + +Conversion to a `bounded_uint` with different bounds is explicit. +The target type's constructor validates that the value falls within `[Min2, Max2]`, throwing `std::domain_error` if it does not. + +=== Comparison Operators + +[source,c++] +---- +friend constexpr auto operator<=>(bounded_uint lhs, bounded_uint rhs) noexcept + -> std::strong_ordering = default; +---- + +Full three-way comparison is supported via `pass:[operator<=>]`, which returns `std::strong_ordering`. +All comparison operators (`<`, `<=`, `>`, `>=`, `==`, `!=`) are available. + +=== Arithmetic Operators + +[source,c++] +---- +template +constexpr auto operator+(bounded_uint lhs, + bounded_uint rhs) -> bounded_uint; + +template +constexpr auto operator-(bounded_uint lhs, + bounded_uint rhs) -> bounded_uint; + +template +constexpr auto operator*(bounded_uint lhs, + bounded_uint rhs) -> bounded_uint; + +template +constexpr auto operator/(bounded_uint lhs, + bounded_uint rhs) -> bounded_uint; + +template +constexpr auto operator%(bounded_uint lhs, + bounded_uint rhs) -> bounded_uint; +---- + +Arithmetic is performed on the underlying `basis_type` values. +The result is then used to construct a new `bounded_uint`, which validates that it falls within `[Min, Max]`. +If the result is out of bounds, the exception thrown depends on the underlying safe unsigned integer operation: + +- `pass:[+]`: Throws `std::overflow_error` if the result exceeds `Max` +- `-`: Throws `std::underflow_error` if the result would be below `Min` +- `pass:[*]`: Throws `std::overflow_error` if the result exceeds `Max` +- `/`: Throws `std::domain_error` if dividing by zero, or `std::domain_error` if the result falls outside `[Min, Max]` +- `%`: Throws `std::domain_error` if the divisor is zero, or `std::domain_error` if the result falls outside `[Min, Max]` + +=== Compound Assignment Operators + +[source,c++] +---- +constexpr auto operator+=(bounded_uint rhs) -> bounded_uint&; +constexpr auto operator-=(bounded_uint rhs) -> bounded_uint&; +constexpr auto operator*=(bounded_uint rhs) -> bounded_uint&; +constexpr auto operator/=(bounded_uint rhs) -> bounded_uint&; +---- + +Compound assignment operators delegate to their corresponding arithmetic operators and follow the same exception behavior. + +=== Increment and Decrement Operators + +[source,c++] +---- +constexpr auto operator++() -> bounded_uint&; +constexpr auto operator++(int) -> bounded_uint; +constexpr auto operator--() -> bounded_uint&; +constexpr auto operator--(int) -> bounded_uint; +---- + +- `++` (pre/post): Adds one to the underlying value and validates the result. Throws `std::overflow_error` if the value is already at `Max`, or `std::domain_error` if the incremented value exceeds the upper bound. +- `--` (pre/post): Subtracts one from the underlying value and validates the result. Throws `std::underflow_error` if the value is already at `Min`, or `std::domain_error` if the decremented value falls below the lower bound. + +=== Mixed-Bounds Operations + +Operations between `bounded_uint` types with different `Min` and `Max` values are compile-time errors. +To perform operations between different bounded types, explicitly convert to the same type first. + +== Constexpr Support + +All operations are `constexpr`-compatible. +Out-of-bounds results at compile time produce a compiler error. diff --git a/doc/modules/ROOT/pages/charconv.adoc b/doc/modules/ROOT/pages/charconv.adoc index 3e41302..8ec0715 100644 --- a/doc/modules/ROOT/pages/charconv.adoc +++ b/doc/modules/ROOT/pages/charconv.adoc @@ -10,8 +10,8 @@ https://www.boost.org/LICENSE_1_0.txt == Description -The library provides character conversion functions for safe integer types using https://www.boost.org/doc/libs/master/libs/charconv/doc/html/charconv.html[Boost.Charconv]. -These functions convert between safe integer types and their string representations. +The library provides character conversion functions for all safe integer types using https://www.boost.org/doc/libs/master/libs/charconv/doc/html/charconv.html[Boost.Charconv]. +These functions convert between safe integer types (including `bounded_uint`) and their string representations. [source,c++] ---- @@ -20,15 +20,15 @@ These functions convert between safe integer types and their string representati namespace boost::safe_numbers { // Convert safe integer to character string -template +template constexpr auto to_chars(char* first, char* last, - unsigned_integer_basis value, + T value, int base = 10) -> charconv::to_chars_result; // Convert character string to safe integer -template +template constexpr auto from_chars(const char* first, const char* last, - unsigned_integer_basis& value, + T& value, int base = 10) -> charconv::from_chars_result; } // namespace boost::safe_numbers @@ -90,13 +90,14 @@ struct from_chars_result [source,c++] ---- -template +template constexpr auto to_chars(char* first, char* last, - unsigned_integer_basis value, + T value, int base = 10) -> charconv::to_chars_result; ---- Converts a safe integer value into a character buffer specified by `[first, last)`. +Works with all safe integer types, including `u8`, `u16`, `u32`, `u64`, `u128`, and `bounded_uint`. === Parameters @@ -119,13 +120,14 @@ Returns `boost::charconv::to_chars_result` with: [source,c++] ---- -template +template constexpr auto from_chars(const char* first, const char* last, - unsigned_integer_basis& value, + T& value, int base = 10) -> charconv::from_chars_result; ---- Parses a string from `[first, last)` and converts it into a safe integer value. +Works with all safe integer types, including `u8`, `u16`, `u32`, `u64`, `u128`, and `bounded_uint`. === Parameters @@ -162,5 +164,10 @@ to_chars (base 2): 11000000111001 from_chars (base 10): 98765 from_chars (base 16): 6699 from_chars (base 2): 26 + +bounded to_chars (percent): 75 +bounded to_chars (port): 8080 +bounded from_chars (percent): 42 +bounded from_chars (port): 443 ---- ==== diff --git a/doc/modules/ROOT/pages/format.adoc b/doc/modules/ROOT/pages/format.adoc index 006b5c1..1b8253f 100644 --- a/doc/modules/ROOT/pages/format.adoc +++ b/doc/modules/ROOT/pages/format.adoc @@ -12,6 +12,7 @@ https://www.boost.org/LICENSE_1_0.txt Boost.SafeNumbers supports formatting with both `` (C++20 and later) and `` (all language standards). The formatters delegate to the underlying integer type's formatter, so all standard integer format specifiers are supported. +Both safe unsigned integers (`u8`, `u16`, `u32`, `u64`, `u128`) and bounded integers (`bounded_uint`) are supported. `std::format` is supported when using C++20 or later with a compiler that has appropriate support: GCC >= 13, Clang >= 18, MSVC >= 19.40. @@ -26,21 +27,19 @@ The formatters delegate to the underlying integer type's formatter, so all stand [source,c++] ---- // -template -struct std::formatter> - : std::formatter +template +struct std::formatter + : std::formatter> { - auto format(const unsigned_integer_basis& val, - std::format_context& ctx) const; + auto format(const T& val, std::format_context& ctx) const; }; // -template -struct fmt::formatter> - : fmt::formatter +template +struct fmt::formatter + : fmt::formatter> { - auto format(const unsigned_integer_basis& val, - fmt::format_context& ctx) const; + auto format(const T& val, fmt::format_context& ctx) const; }; ---- @@ -163,6 +162,11 @@ Padding and Alignment: Fill Character: *****12345 12345_____ + +Bounded Integers: +75 + 8080 +0x1bb ---- ==== diff --git a/doc/modules/ROOT/pages/limits.adoc b/doc/modules/ROOT/pages/limits.adoc index 3914efb..fe9aa3a 100644 --- a/doc/modules/ROOT/pages/limits.adoc +++ b/doc/modules/ROOT/pages/limits.adoc @@ -198,3 +198,68 @@ static constexpr T denorm_min() noexcept; ---- Returns `Tpass:[{0}]` (not meaningful for integer types). + +== Bounded Integer Specialization + +A partial specialization of `std::numeric_limits` is provided for all `bounded_uint` types. +Static member constants delegate to the underlying hardware type's `std::numeric_limits` specialization, just as they do for the safe unsigned integer types. + +The key difference is in the `min()` and `max()` member functions: rather than returning the hardware type's full range, they return the compile-time bounds `Min` and `Max`. + +[source,c++] +---- +namespace std { + +template +class numeric_limits> +{ + // Static member constants delegate to std::numeric_limits + // ... + + // Returns bounded_uint constructed from Min + static constexpr bounded_uint min(); + + // Returns bounded_uint constructed from Max + static constexpr bounded_uint max(); + + // Same as min() for unsigned types + static constexpr bounded_uint lowest(); + + // Not meaningful for integer types; returns min() + static constexpr bounded_uint epsilon(); + static constexpr bounded_uint round_error(); + static constexpr bounded_uint infinity(); + static constexpr bounded_uint quiet_NaN(); + static constexpr bounded_uint signaling_NaN(); + static constexpr bounded_uint denorm_min(); +}; + +} // namespace std +---- + +NOTE: The non-applicable member functions (`epsilon`, `round_error`, `infinity`, `quiet_NaN`, `signaling_NaN`, `denorm_min`) return `min()` rather than a zero-constructed value, since zero may fall outside the valid range for types with a non-zero lower bound. + +=== Example + +.This https://github.com/boostorg/safe_numbers/blob/develop/examples/bounded_limits.cpp[example] demonstrates `std::numeric_limits` with bounded integer types. +==== +[source, c++] +---- +include::example$bounded_limits.cpp[] +---- + +Output: +---- +percent type: + min: 0 + max: 100 + lowest: 0 + digits: 8 + +port type: + min: 1 + max: 65535 + lowest: 1 + digits: 16 +---- +==== diff --git a/doc/modules/ROOT/pages/unsigned_integers.adoc b/doc/modules/ROOT/pages/unsigned_integers.adoc index 882bbe5..1a1480f 100644 --- a/doc/modules/ROOT/pages/unsigned_integers.adoc +++ b/doc/modules/ROOT/pages/unsigned_integers.adoc @@ -53,7 +53,7 @@ public: // Conversion to underlying types template - explicit constexpr operator OtherBasis() const noexcept; + explicit constexpr operator OtherBasis() const; // Comparison operators friend constexpr auto operator<=>(unsigned_integer_basis lhs, unsigned_integer_basis rhs) noexcept @@ -239,11 +239,13 @@ Constructing from `bool` is a compile-time error. [source,c++] ---- template -explicit constexpr operator OtherBasis() const noexcept; +explicit constexpr operator OtherBasis() const; ---- Conversion to other unsigned integral types is explicit. -Narrowing conversions cause a compile-time error. +Widening conversions (e.g., `u8` to `u16`) always succeed. +Narrowing conversions (e.g., `u32` to `u16`) perform a runtime bounds check: if the value exceeds the maximum representable value of the target type, `std::domain_error` is thrown. +This allows safe narrowing when the value is known to fit at runtime. === Comparison Operators diff --git a/examples/bit.cpp b/examples/bit.cpp index c3cf48a..5df8022 100644 --- a/examples/bit.cpp +++ b/examples/bit.cpp @@ -3,6 +3,7 @@ // https://www.boost.org/LICENSE_1_0.txt #include +#include #include #include @@ -43,5 +44,22 @@ int main() std::cout << std::hex; std::cout << "byteswap(0x12345678) = 0x" << static_cast(byteswap(w)) << '\n'; + std::cout << std::dec << '\n'; + + // Bounded integer types work the same way + using byte_val = bounded_uint<0u, 255u>; + using word_val = bounded_uint<0u, 65535u>; + + const auto bv = byte_val{0b0010'1000u}; // 40 + std::cout << "bounded has_single_bit(40) = " << has_single_bit(bv) << '\n'; + std::cout << "bounded bit_ceil(40) = " << static_cast(static_cast(bit_ceil(bv))) << '\n'; + std::cout << "bounded bit_floor(40) = " << static_cast(static_cast(bit_floor(bv))) << '\n'; + std::cout << "bounded bit_width(40) = " << bit_width(bv) << '\n'; + std::cout << "bounded popcount(40) = " << popcount(bv) << '\n'; + + const auto wv = word_val{0x0F00u}; + std::cout << "bounded countl_zero(0x0F00) = " << countl_zero(wv) << '\n'; + std::cout << "bounded popcount(0x0F00) = " << popcount(wv) << '\n'; + return 0; } diff --git a/examples/bounded_limits.cpp b/examples/bounded_limits.cpp new file mode 100644 index 0000000..d79cce8 --- /dev/null +++ b/examples/bounded_limits.cpp @@ -0,0 +1,33 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include +#include +#include +#include +#include + +int main() +{ + using boost::safe_numbers::bounded_uint; + + using percent = bounded_uint<0u, 100u>; + + std::cout << "percent type:" << std::endl; + std::cout << " min: " << std::numeric_limits::min() << std::endl; + std::cout << " max: " << std::numeric_limits::max() << std::endl; + std::cout << " lowest: " << std::numeric_limits::lowest() << std::endl; + std::cout << " digits: " << std::numeric_limits::digits << std::endl; + std::cout << std::endl; + + using port = bounded_uint<1u, 65535u>; + + std::cout << "port type:" << std::endl; + std::cout << " min: " << std::numeric_limits::min() << std::endl; + std::cout << " max: " << std::numeric_limits::max() << std::endl; + std::cout << " lowest: " << std::numeric_limits::lowest() << std::endl; + std::cout << " digits: " << std::numeric_limits::digits << std::endl; + + return 0; +} diff --git a/examples/charconv.cpp b/examples/charconv.cpp index fe0689f..9630ef8 100644 --- a/examples/charconv.cpp +++ b/examples/charconv.cpp @@ -3,6 +3,7 @@ // https://www.boost.org/LICENSE_1_0.txt #include +#include #include #include #include @@ -69,5 +70,47 @@ int main() std::cout << "from_chars (base 2): " << static_cast(bin_value) << '\n'; } + std::cout << '\n'; + + // Bounded integer types work the same way + using percent = bounded_uint<0u, 100u>; + using port = bounded_uint<1u, 65535u>; + + // to_chars with bounded types + auto pct = percent{75u}; + result = to_chars(buffer, buffer + sizeof(buffer), pct); + if (result) + { + *result.ptr = '\0'; + std::cout << "bounded to_chars (percent): " << buffer << '\n'; + } + + auto p = port{8080u}; + result = to_chars(buffer, buffer + sizeof(buffer), p); + if (result) + { + *result.ptr = '\0'; + std::cout << "bounded to_chars (port): " << buffer << '\n'; + } + + // from_chars with bounded types + const char* pct_str = "42"; + auto parsed_pct = percent{0u}; + + parse_result = from_chars(pct_str, pct_str + std::strlen(pct_str), parsed_pct); + if (parse_result) + { + std::cout << "bounded from_chars (percent): " << static_cast(static_cast(parsed_pct)) << '\n'; + } + + const char* port_str = "443"; + auto parsed_port = port{1u}; + + parse_result = from_chars(port_str, port_str + std::strlen(port_str), parsed_port); + if (parse_result) + { + std::cout << "bounded from_chars (port): " << static_cast(static_cast(parsed_port)) << '\n'; + } + return 0; } diff --git a/examples/fmt_format.cpp b/examples/fmt_format.cpp index 1626a72..620c5d4 100644 --- a/examples/fmt_format.cpp +++ b/examples/fmt_format.cpp @@ -7,6 +7,7 @@ #if __has_include() #include +#include #include #include #include @@ -48,7 +49,16 @@ int main() // Fill character std::cout << "Fill Character:\n"; std::cout << fmt::format("{:*>10}", val1) << '\n'; - std::cout << fmt::format("{:_<10}", val1) << '\n'; + std::cout << fmt::format("{:_<10}", val1) << "\n\n"; + + // Bounded integer types + using percent = bounded_uint<0u, 100u>; + using port = bounded_uint<1u, 65535u>; + + std::cout << "Bounded Integers:\n"; + std::cout << fmt::format("{}", percent{75u}) << '\n'; + std::cout << fmt::format("{:>8}", port{8080u}) << '\n'; + std::cout << fmt::format("{:#x}", port{443u}) << '\n'; return 0; } diff --git a/include/boost/safe_numbers.hpp b/include/boost/safe_numbers.hpp index 3bcfe15..d497702 100644 --- a/include/boost/safe_numbers.hpp +++ b/include/boost/safe_numbers.hpp @@ -6,6 +6,7 @@ #define BOOST_SAFENUMBERS_HPP #include +#include #include #include #include diff --git a/include/boost/safe_numbers/bit.hpp b/include/boost/safe_numbers/bit.hpp index df892a8..2cb9424 100644 --- a/include/boost/safe_numbers/bit.hpp +++ b/include/boost/safe_numbers/bit.hpp @@ -22,108 +22,108 @@ BOOST_SAFE_NUMBERS_EXPORT template constexpr auto has_single_bit(const UnsignedInt x) noexcept -> bool { using boost::core::has_single_bit; - using basis_type = typename UnsignedInt::basis_type; + using underlying_type = detail::underlying_type_t; - return has_single_bit(static_cast(x)); + return has_single_bit(static_cast(x)); } BOOST_SAFE_NUMBERS_EXPORT template constexpr auto bit_ceil(const UnsignedInt x) noexcept -> UnsignedInt { using boost::core::bit_ceil; - using basis_type = typename UnsignedInt::basis_type; + using underlying_type = detail::underlying_type_t; - return static_cast(bit_ceil(static_cast(x))); + return UnsignedInt{bit_ceil(static_cast(x))}; } BOOST_SAFE_NUMBERS_EXPORT template constexpr auto bit_floor(const UnsignedInt x) noexcept -> UnsignedInt { using boost::core::bit_floor; - using basis_type = typename UnsignedInt::basis_type; + using underlying_type = detail::underlying_type_t; - return static_cast(bit_floor(static_cast(x))); + return UnsignedInt{bit_floor(static_cast(x))}; } BOOST_SAFE_NUMBERS_EXPORT template constexpr auto bit_width(const UnsignedInt x) noexcept -> int { using boost::core::bit_width; - using basis_type = typename UnsignedInt::basis_type; + using underlying_type = detail::underlying_type_t; - return static_cast(bit_width(static_cast(x))); + return static_cast(bit_width(static_cast(x))); } -BOOST_SAFE_NUMBERS_EXPORT template +BOOST_SAFE_NUMBERS_EXPORT template constexpr auto rotl(const UnsignedInt x, const int s) noexcept -> UnsignedInt { using boost::core::rotl; - using basis_type = typename UnsignedInt::basis_type; + using underlying_type = detail::underlying_type_t; - return static_cast(rotl(static_cast(x), s)); + return UnsignedInt{rotl(static_cast(x), s)}; } -BOOST_SAFE_NUMBERS_EXPORT template +BOOST_SAFE_NUMBERS_EXPORT template constexpr auto rotr(const UnsignedInt x, const int s) noexcept -> UnsignedInt { using boost::core::rotr; - using basis_type = typename UnsignedInt::basis_type; + using underlying_type = detail::underlying_type_t; - return static_cast(rotr(static_cast(x), s)); + return UnsignedInt{rotr(static_cast(x), s)}; } BOOST_SAFE_NUMBERS_EXPORT template constexpr auto countl_zero(const UnsignedInt x) noexcept -> int { using boost::core::countl_zero; - using basis_type = typename UnsignedInt::basis_type; + using underlying_type = detail::underlying_type_t; - return countl_zero(static_cast(x)); + return countl_zero(static_cast(x)); } BOOST_SAFE_NUMBERS_EXPORT template constexpr auto countl_one(const UnsignedInt x) noexcept -> int { using boost::core::countl_one; - using basis_type = typename UnsignedInt::basis_type; + using underlying_type = detail::underlying_type_t; - return countl_one(static_cast(x)); + return countl_one(static_cast(x)); } BOOST_SAFE_NUMBERS_EXPORT template constexpr auto countr_zero(const UnsignedInt x) noexcept -> int { using boost::core::countr_zero; - using basis_type = typename UnsignedInt::basis_type; + using underlying_type = detail::underlying_type_t; - return countr_zero(static_cast(x)); + return countr_zero(static_cast(x)); } BOOST_SAFE_NUMBERS_EXPORT template constexpr auto countr_one(const UnsignedInt x) noexcept -> int { using boost::core::countr_one; - using basis_type = typename UnsignedInt::basis_type; + using underlying_type = detail::underlying_type_t; - return countr_one(static_cast(x)); + return countr_one(static_cast(x)); } BOOST_SAFE_NUMBERS_EXPORT template constexpr auto popcount(const UnsignedInt x) noexcept -> int { using boost::core::popcount; - using basis_type = typename UnsignedInt::basis_type; + using underlying_type = detail::underlying_type_t; - return popcount(static_cast(x)); + return popcount(static_cast(x)); } -BOOST_SAFE_NUMBERS_EXPORT template +BOOST_SAFE_NUMBERS_EXPORT template constexpr auto byteswap(const UnsignedInt x) noexcept -> UnsignedInt { using boost::core::byteswap; - using basis_type = typename UnsignedInt::basis_type; + using underlying_type = detail::underlying_type_t; - return static_cast(byteswap(static_cast(x))); + return UnsignedInt{byteswap(static_cast(x))}; } } // namespace boost::safe_numbers diff --git a/include/boost/safe_numbers/bounded_integers.hpp b/include/boost/safe_numbers/bounded_integers.hpp new file mode 100644 index 0000000..ba31b12 --- /dev/null +++ b/include/boost/safe_numbers/bounded_integers.hpp @@ -0,0 +1,274 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#ifndef BOOST_SAFE_NUMBERS_BOUNDED_INTEGERS_HPP +#define BOOST_SAFE_NUMBERS_BOUNDED_INTEGERS_HPP + +#include +#include +#include +#include +#include + +#ifndef BOOST_SAFE_NUMBERS_BUILD_MODULE + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#endif // BOOST_SAFE_NUMBERS_BUILD_MODULE + +namespace boost::safe_numbers { + +template + requires (detail::valid_bound && + detail::valid_bound && + detail::raw_value(Max) > detail::raw_value(Min)) +class bounded_uint +{ +public: + + using basis_type = std::conditional_t<(std::numeric_limits::max() >= detail::raw_value(Max)), u8, + std::conditional_t<(std::numeric_limits::max() >= detail::raw_value(Max)), u16, + std::conditional_t<(std::numeric_limits::max() >= detail::raw_value(Max)), u32, + std::conditional_t<(std::numeric_limits::max() >= detail::raw_value(Max)), u64, u128>>>>; + +private: + + using underlying_type = detail::underlying_type_t; + basis_type basis_ {static_cast(detail::raw_value(Min))}; + +public: + + explicit constexpr bounded_uint(const basis_type val) + { + constexpr auto min_val {basis_type{static_cast(detail::raw_value(Min))}}; + constexpr auto max_val {basis_type{static_cast(detail::raw_value(Max))}}; + + if (val < min_val || val > max_val) + { + BOOST_THROW_EXCEPTION(std::domain_error("Construction from value outside the bounds")); + } + + basis_ = val; + } + + explicit constexpr bounded_uint(const underlying_type val) : bounded_uint{basis_type{val}} {} + + template + requires (detail::is_unsigned_library_type_v || detail::is_fundamental_unsigned_integral_v) + [[nodiscard]] explicit constexpr operator OtherBasis() const + { + const auto raw {static_cast>(basis_)}; + + if constexpr (sizeof(OtherBasis) < sizeof(basis_type)) + { + using raw_other = detail::underlying_type_t; + if (raw > static_cast>(std::numeric_limits::max())) + { + BOOST_THROW_EXCEPTION(std::domain_error("Overflow in conversion to smaller type")); + } + + return static_cast(static_cast(raw)); + } + else + { + return static_cast(raw); + } + } + + template + [[nodiscard]] explicit constexpr operator bounded_uint() const + { + using target_basis = typename bounded_uint::basis_type; + const auto raw {static_cast>(basis_)}; + return bounded_uint{static_cast(raw)}; + } + + [[nodiscard]] explicit constexpr operator basis_type() const noexcept { return basis_; } + + [[nodiscard]] explicit constexpr operator underlying_type() const noexcept { return static_cast(basis_); } + + [[nodiscard]] friend constexpr auto operator<=>(bounded_uint lhs, bounded_uint rhs) noexcept + -> std::strong_ordering = default; + + + constexpr auto operator+=(bounded_uint rhs) -> bounded_uint&; + + constexpr auto operator-=(bounded_uint rhs) -> bounded_uint&; + + constexpr auto operator*=(bounded_uint rhs) -> bounded_uint&; + + constexpr auto operator/=(bounded_uint rhs) -> bounded_uint&; + + constexpr auto operator++() -> bounded_uint&; + + constexpr auto operator++(int) -> bounded_uint; + + constexpr auto operator--() -> bounded_uint&; + + constexpr auto operator--(int) -> bounded_uint; +}; + +template +[[nodiscard]] constexpr auto operator+(const bounded_uint lhs, + const bounded_uint rhs) -> bounded_uint +{ + using basis = typename bounded_uint::basis_type; + return bounded_uint{static_cast(lhs) + static_cast(rhs)}; +} + +template +[[nodiscard]] constexpr auto operator-(const bounded_uint lhs, + const bounded_uint rhs) -> bounded_uint +{ + using basis = typename bounded_uint::basis_type; + return bounded_uint{static_cast(lhs) - static_cast(rhs)}; +} + +template +[[nodiscard]] constexpr auto operator*(const bounded_uint lhs, + const bounded_uint rhs) -> bounded_uint +{ + using basis = typename bounded_uint::basis_type; + return bounded_uint{static_cast(lhs) * static_cast(rhs)}; +} + +template +[[nodiscard]] constexpr auto operator/(const bounded_uint lhs, + const bounded_uint rhs) -> bounded_uint +{ + using basis = typename bounded_uint::basis_type; + return bounded_uint{static_cast(lhs) / static_cast(rhs)}; +} + +template +[[nodiscard]] constexpr auto operator%(const bounded_uint lhs, + const bounded_uint rhs) -> bounded_uint +{ + using basis = typename bounded_uint::basis_type; + return bounded_uint{static_cast(lhs) % static_cast(rhs)}; +} + +template + requires (detail::valid_bound && + detail::valid_bound && + detail::raw_value(Max) > detail::raw_value(Min)) +constexpr auto bounded_uint::operator+=(bounded_uint rhs) -> bounded_uint& +{ + *this = *this + rhs; + return *this; +} + +template + requires (detail::valid_bound && + detail::valid_bound && + detail::raw_value(Max) > detail::raw_value(Min)) +constexpr auto bounded_uint::operator-=(bounded_uint rhs) -> bounded_uint& +{ + *this = *this - rhs; + return *this; +} + +template + requires (detail::valid_bound && + detail::valid_bound && + detail::raw_value(Max) > detail::raw_value(Min)) +constexpr auto bounded_uint::operator*=(bounded_uint rhs) -> bounded_uint& +{ + *this = *this * rhs; + return *this; +} + +template + requires (detail::valid_bound && + detail::valid_bound && + detail::raw_value(Max) > detail::raw_value(Min)) +constexpr auto bounded_uint::operator/=(bounded_uint rhs) -> bounded_uint& +{ + *this = *this / rhs; + return *this; +} + +template + requires (detail::valid_bound && + detail::valid_bound && + detail::raw_value(Max) > detail::raw_value(Min)) +constexpr auto bounded_uint::operator++() -> bounded_uint& +{ + auto val {static_cast(*this) + basis_type{1}}; + *this = bounded_uint{val}; + return *this; +} + +template + requires (detail::valid_bound && + detail::valid_bound && + detail::raw_value(Max) > detail::raw_value(Min)) +constexpr auto bounded_uint::operator++(int) -> bounded_uint +{ + auto tmp {*this}; + ++(*this); + return tmp; +} + +template + requires (detail::valid_bound && + detail::valid_bound && + detail::raw_value(Max) > detail::raw_value(Min)) +constexpr auto bounded_uint::operator--() -> bounded_uint& +{ + auto val {static_cast(*this) - basis_type{1}}; + *this = bounded_uint{val}; + return *this; +} + +template + requires (detail::valid_bound && + detail::valid_bound && + detail::raw_value(Max) > detail::raw_value(Min)) +constexpr auto bounded_uint::operator--(int) -> bounded_uint +{ + auto tmp {*this}; + --(*this); + return tmp; +} + +} // namespace boost::safe_numbers + +#define BOOST_SAFE_NUMBERS_DEFINE_MIXED_BOUNDED_UINT_OP(OP_NAME, OP_SYMBOL) \ +template \ + requires (LHSMin != RHSMin || LHSMax != RHSMax) \ +constexpr auto OP_SYMBOL(const boost::safe_numbers::bounded_uint, \ + const boost::safe_numbers::bounded_uint) \ +{ \ + static_assert(boost::safe_numbers::detail::dependent_false< \ + boost::safe_numbers::bounded_uint, \ + boost::safe_numbers::bounded_uint>, \ + "Can not perform " OP_NAME " between bounded_uint types with different bounds. " \ + "Both operands must have the same Min and Max."); \ + \ + return boost::safe_numbers::bounded_uint( \ + typename boost::safe_numbers::bounded_uint::basis_type{0}); \ +} + +namespace boost::safe_numbers { + +BOOST_SAFE_NUMBERS_DEFINE_MIXED_BOUNDED_UINT_OP("addition", operator+) +BOOST_SAFE_NUMBERS_DEFINE_MIXED_BOUNDED_UINT_OP("subtraction", operator-) +BOOST_SAFE_NUMBERS_DEFINE_MIXED_BOUNDED_UINT_OP("multiplication", operator*) +BOOST_SAFE_NUMBERS_DEFINE_MIXED_BOUNDED_UINT_OP("division", operator/) +BOOST_SAFE_NUMBERS_DEFINE_MIXED_BOUNDED_UINT_OP("modulo", operator%) + +} // namespace boost::safe_numbers + +#undef BOOST_SAFE_NUMBERS_DEFINE_MIXED_BOUNDED_UINT_OP + +#endif // BOOST_SAFE_NUMBERS_BOUNDED_INTEGERS_HPP diff --git a/include/boost/safe_numbers/charconv.hpp b/include/boost/safe_numbers/charconv.hpp index a8f4828..2103152 100644 --- a/include/boost/safe_numbers/charconv.hpp +++ b/include/boost/safe_numbers/charconv.hpp @@ -5,8 +5,7 @@ #ifndef BOOST_SAFE_NUMBERS_CHARCONV_HPP #define BOOST_SAFE_NUMBERS_CHARCONV_HPP -#include -#include +#include #include #ifndef BOOST_SAFE_NUMBERS_BUILD_MODULE @@ -17,22 +16,25 @@ namespace boost::safe_numbers { -template -constexpr auto from_chars(const char* first, const char* last, detail::unsigned_integer_basis& value, int base = 10) +template +constexpr auto from_chars(const char* first, const char* last, T& value, int base = 10) -> charconv::from_chars_result { - BasisType result {}; + using underlying_type = detail::underlying_type_t; + + underlying_type result {}; const auto r {charconv::from_chars(first, last, result, base)}; - value = detail::unsigned_integer_basis{result}; + value = T{result}; return r; } -template -constexpr auto to_chars(char* first, char* last, const detail::unsigned_integer_basis value, int base = 10) +template +constexpr auto to_chars(char* first, char* last, const T value, int base = 10) -> charconv::to_chars_result { - return charconv::to_chars(first, last, static_cast(value), base); + using underlying_type = detail::underlying_type_t; + return charconv::to_chars(first, last, static_cast(value), base); } } // namespace boost::safe_numbers diff --git a/include/boost/safe_numbers/detail/fwd.hpp b/include/boost/safe_numbers/detail/fwd.hpp new file mode 100644 index 0000000..d0945f5 --- /dev/null +++ b/include/boost/safe_numbers/detail/fwd.hpp @@ -0,0 +1,132 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#ifndef BOOST_SAFE_NUMBERS_DETAIL_FWD_HPP +#define BOOST_SAFE_NUMBERS_DETAIL_FWD_HPP + +#include +#include + +#ifndef BOOST_SAFE_NUMBERS_BUILD_MODULE + +#include +#include + +#endif // BOOST_SAFE_NUMBERS_BUILD_MODULE + +namespace boost::safe_numbers::detail { + +// Forward declaration of unsigned_integer_basis +template +class unsigned_integer_basis; + +// is_fundamental_unsigned_integral + +namespace impl { + +template +struct is_fundamental_unsigned_integral : std::bool_constant || std::is_same_v, int128::uint128_t>> {}; + +} // namespace impl + +template +inline constexpr bool is_fundamental_unsigned_integral_v = impl::is_fundamental_unsigned_integral::value; + +// is_unsigned_library_type (base + unsigned_integer_basis specialization) + +namespace impl { + +template +struct is_unsigned_library_type : std::false_type {}; + +template +struct is_unsigned_library_type> : std::true_type {}; + +} // namespace impl + +template +inline constexpr bool is_unsigned_library_type_v = impl::is_unsigned_library_type::value; + +// underlying type trait (base + unsigned_integer_basis specialization) + +namespace impl { + +template +struct underlying +{ + using type = std::remove_cv_t>; +}; + +template +struct underlying> +{ + using type = T; +}; + +} // namespace impl + +template +using underlying_type_t = typename impl::underlying::type; + +// valid_bound concept + +template +concept valid_bound = !std::is_same_v && (is_unsigned_library_type_v || is_fundamental_unsigned_integral_v); + +// raw_value function + +template + requires valid_bound +consteval auto raw_value(T val) noexcept +{ + if constexpr (is_unsigned_library_type_v) + { + return static_cast>(val); + } + else + { + return val; + } +} + +} // namespace boost::safe_numbers::detail + +// Constrained forward declaration of bounded_uint +namespace boost::safe_numbers { + +template + requires (detail::valid_bound && + detail::valid_bound && + detail::raw_value(Max) > detail::raw_value(Min)) +class bounded_uint; + +} // namespace boost::safe_numbers + +// bounded_uint specialization of is_unsigned_library_type +namespace boost::safe_numbers::detail::impl { + +template +struct is_unsigned_library_type> : std::true_type {}; + +} // namespace boost::safe_numbers::detail::impl + +// is_bounded_type trait +namespace boost::safe_numbers::detail { + +namespace impl { + +template +struct is_bounded_type : std::false_type {}; + +template +struct is_bounded_type> : std::true_type {}; + +} // namespace impl + +template +inline constexpr bool is_bounded_type_v = impl::is_bounded_type::value; + +} // namespace boost::safe_numbers::detail + +#endif // BOOST_SAFE_NUMBERS_DETAIL_FWD_HPP diff --git a/include/boost/safe_numbers/detail/int128/detail/int128_imp.hpp b/include/boost/safe_numbers/detail/int128/detail/int128_imp.hpp index 42e707b..1ac0ce6 100644 --- a/include/boost/safe_numbers/detail/int128/detail/int128_imp.hpp +++ b/include/boost/safe_numbers/detail/int128/detail/int128_imp.hpp @@ -17,6 +17,7 @@ #include #include +#include #endif @@ -1100,6 +1101,20 @@ BOOST_SAFE_NUMBERS_DETAIL_INT128_BUILTIN_CONSTEXPR bool operator>=(const T, cons #endif // BOOST_SAFE_NUMBERS_DETAIL_INT128_HAS_INT128 +//===================================== +// Three-way Comparison Operator +//===================================== + +BOOST_SAFE_NUMBERS_DETAIL_INT128_EXPORT constexpr std::strong_ordering operator<=>(const int128_t lhs, const int128_t rhs) noexcept +{ + if (lhs.high != rhs.high) + { + return lhs.high <=> rhs.high; + } + + return lhs.low <=> rhs.low; +} + //===================================== // Not Operator //===================================== diff --git a/include/boost/safe_numbers/detail/int128/detail/uint128_imp.hpp b/include/boost/safe_numbers/detail/int128/detail/uint128_imp.hpp index 0f5d0bd..c2f57cd 100644 --- a/include/boost/safe_numbers/detail/int128/detail/uint128_imp.hpp +++ b/include/boost/safe_numbers/detail/int128/detail/uint128_imp.hpp @@ -18,6 +18,7 @@ #include #include #include +#include #endif @@ -1216,6 +1217,20 @@ BOOST_SAFE_NUMBERS_DETAIL_INT128_BUILTIN_CONSTEXPR bool operator>=(const T, cons #endif // BOOST_SAFE_NUMBERS_DETAIL_INT128_HAS_INT128 +//===================================== +// Three-way Comparison Operator +//===================================== + +BOOST_SAFE_NUMBERS_DETAIL_INT128_EXPORT constexpr std::strong_ordering operator<=>(const uint128_t lhs, const uint128_t rhs) noexcept +{ + if (lhs.high != rhs.high) + { + return lhs.high <=> rhs.high; + } + + return lhs.low <=> rhs.low; +} + //===================================== // Not Operator //===================================== diff --git a/include/boost/safe_numbers/detail/type_traits.hpp b/include/boost/safe_numbers/detail/type_traits.hpp index f6e1f64..210dce1 100644 --- a/include/boost/safe_numbers/detail/type_traits.hpp +++ b/include/boost/safe_numbers/detail/type_traits.hpp @@ -5,8 +5,7 @@ #ifndef BOOST_SAFE_NUMBERS_DETAIL_TYPE_TRAITS_HPP #define BOOST_SAFE_NUMBERS_DETAIL_TYPE_TRAITS_HPP -#include -#include +#include #ifndef BOOST_SAFE_NUMBERS_BUILD_MODULE @@ -17,8 +16,7 @@ namespace boost::safe_numbers::detail { -template -class unsigned_integer_basis; +// is_library_type namespace impl { @@ -28,6 +26,9 @@ struct is_library_type : std::false_type {}; template struct is_library_type> : std::true_type {}; +template +struct is_library_type> : std::true_type {}; + } // namespace impl template @@ -36,41 +37,24 @@ inline constexpr bool is_library_type_v = impl::is_library_type::value; template concept library_type = is_library_type_v; -namespace impl { - -template -struct is_unsigned_library_type : std::false_type {}; - template -struct is_unsigned_library_type> : std::true_type {}; - -} // namespace impl +concept unsigned_library_type = is_unsigned_library_type_v; template -inline constexpr bool is_unsigned_library_type_v = impl::is_unsigned_library_type::value; +concept non_bounded_unsigned_library_type = is_unsigned_library_type_v && !is_bounded_type_v; -template -concept unsigned_library_type = is_unsigned_library_type_v; +// underlying specialization for bounded_uint namespace impl { -template -struct underlying -{ - using type = std::remove_cv_t>; -}; - -template -struct underlying> +template +struct underlying> { - using type = T; + using type = typename underlying::basis_type>::type; }; } // namespace impl -template -using underlying_type_t = typename impl::underlying::type; - } // namespace boost::safe_numbers::detail #endif // BOOST_SAFE_NUMBERS_DETAIL_TYPE_TRAITS_HPP diff --git a/include/boost/safe_numbers/detail/unsigned_integer_basis.hpp b/include/boost/safe_numbers/detail/unsigned_integer_basis.hpp index 42a876a..4555f9c 100644 --- a/include/boost/safe_numbers/detail/unsigned_integer_basis.hpp +++ b/include/boost/safe_numbers/detail/unsigned_integer_basis.hpp @@ -51,7 +51,9 @@ class unsigned_integer_basis } template - [[nodiscard]] explicit constexpr operator OtherBasis() const noexcept; + [[nodiscard]] explicit constexpr operator OtherBasis() const; + + explicit constexpr operator BasisType() const noexcept { return basis_;} [[nodiscard]] friend constexpr auto operator<=>(unsigned_integer_basis lhs, unsigned_integer_basis rhs) noexcept -> std::strong_ordering = default; @@ -82,11 +84,14 @@ class unsigned_integer_basis template template -constexpr unsigned_integer_basis::operator OtherBasis() const noexcept +constexpr unsigned_integer_basis::operator OtherBasis() const { if constexpr (sizeof(OtherBasis) < sizeof(BasisType)) { - static_assert(dependent_false, "Narrowing conversions are not allowed"); + if (basis_ > static_cast(std::numeric_limits::max())) + { + BOOST_THROW_EXCEPTION(std::domain_error("Overflow in conversion to smaller type")); + } } return static_cast(basis_); diff --git a/include/boost/safe_numbers/fmt_format.hpp b/include/boost/safe_numbers/fmt_format.hpp index 298a2db..68a25f6 100644 --- a/include/boost/safe_numbers/fmt_format.hpp +++ b/include/boost/safe_numbers/fmt_format.hpp @@ -6,7 +6,7 @@ #define BOOST_SAFE_NUMBERS_FMT_FORMAT_HPP #include -#include +#include #include #ifndef BOOST_SAFE_NUMBERS_BUILD_MODULE @@ -15,14 +15,15 @@ #endif // BOOST_SAFE_NUMBERS_BUILD_MODULE -template -struct fmt::formatter> - : fmt::formatter +template +struct fmt::formatter + : fmt::formatter> { - auto format(const boost::safe_numbers::detail::unsigned_integer_basis& val, - fmt::format_context& ctx) const + using underlying_type = boost::safe_numbers::detail::underlying_type_t; + + auto format(const T& val, fmt::format_context& ctx) const { - return fmt::formatter::format(static_cast(val), ctx); + return fmt::formatter::format(static_cast(val), ctx); } }; diff --git a/include/boost/safe_numbers/format.hpp b/include/boost/safe_numbers/format.hpp index 33204a5..af73a46 100644 --- a/include/boost/safe_numbers/format.hpp +++ b/include/boost/safe_numbers/format.hpp @@ -7,7 +7,7 @@ #include #include -#include +#include #ifdef BOOST_SAFE_NUMBERS_DETAIL_INT128_HAS_FORMAT @@ -18,14 +18,15 @@ #endif // BOOST_SAFE_NUMBERS_BUILD_MODULE -template -struct std::formatter> - : std::formatter +template +struct std::formatter + : std::formatter> { - auto format(const boost::safe_numbers::detail::unsigned_integer_basis& val, - std::format_context& ctx) const + using underlying_type = boost::safe_numbers::detail::underlying_type_t; + + auto format(const T& val, std::format_context& ctx) const { - return std::formatter::format(static_cast(val), ctx); + return std::formatter::format(static_cast(val), ctx); } }; diff --git a/include/boost/safe_numbers/iostream.hpp b/include/boost/safe_numbers/iostream.hpp index ccdada5..dd7f8e3 100644 --- a/include/boost/safe_numbers/iostream.hpp +++ b/include/boost/safe_numbers/iostream.hpp @@ -76,6 +76,13 @@ auto operator<<(std::basic_ostream& os, const LibType& v) -> std: return os; } -} // namespace boost::safe_numbers +} // namespace boost::safe_numbers::detail + +namespace boost::safe_numbers { + +using detail::operator<<; +using detail::operator>>; + +} // namespace boost::safe_numbers #endif // BOOST_SAFE_NUMBERS_IOSTREAM_HPP diff --git a/include/boost/safe_numbers/limits.hpp b/include/boost/safe_numbers/limits.hpp index d5df086..5711fc3 100644 --- a/include/boost/safe_numbers/limits.hpp +++ b/include/boost/safe_numbers/limits.hpp @@ -7,6 +7,7 @@ #include #include +#include #ifndef BOOST_SAFE_NUMBERS_BUILD_MODULE @@ -89,6 +90,52 @@ template <> class numeric_limits : public boost::safe_numbers::detail::numeric_limits_impl {}; +template +class numeric_limits> +{ + using type = boost::safe_numbers::bounded_uint; + using underlying_type = boost::safe_numbers::detail::underlying_type_t; + +public: + static constexpr bool is_specialized = std::numeric_limits::is_specialized; + static constexpr bool is_signed = std::numeric_limits::is_signed; + static constexpr bool is_integer = std::numeric_limits::is_integer; + static constexpr bool is_exact = std::numeric_limits::is_exact; + static constexpr bool has_infinity = std::numeric_limits::has_infinity; + static constexpr bool has_quiet_NaN = std::numeric_limits::has_quiet_NaN; + static constexpr bool has_signaling_NaN = std::numeric_limits::has_signaling_NaN; + + #if ((!defined(_MSC_VER) && (__cplusplus <= 202002L)) || (defined(_MSC_VER) && (_MSVC_LANG <= 202002L))) + static constexpr std::float_denorm_style has_denorm = std::numeric_limits::has_denorm; + static constexpr bool has_denorm_loss = std::numeric_limits::has_denorm_loss; + #endif + + static constexpr std::float_round_style round_style = std::numeric_limits::round_style; + static constexpr bool is_iec559 = std::numeric_limits::is_iec559; + static constexpr bool is_bounded = std::numeric_limits::is_bounded; + static constexpr bool is_modulo = std::numeric_limits::is_modulo; + static constexpr int digits = std::numeric_limits::digits; + static constexpr int digits10 = std::numeric_limits::digits10; + static constexpr int max_digits10 = std::numeric_limits::max_digits10; + static constexpr int radix = std::numeric_limits::radix; + static constexpr int min_exponent = std::numeric_limits::min_exponent; + static constexpr int min_exponent10 = std::numeric_limits::min_exponent10; + static constexpr int max_exponent = std::numeric_limits::max_exponent; + static constexpr int max_exponent10 = std::numeric_limits::max_exponent10; + static constexpr bool traps = std::numeric_limits::traps; + static constexpr bool tinyness_before = std::numeric_limits::tinyness_before; + + static constexpr type min() { return type{static_cast(boost::safe_numbers::detail::raw_value(Min))}; } + static constexpr type max() { return type{static_cast(boost::safe_numbers::detail::raw_value(Max))}; } + static constexpr type lowest() { return min(); } + static constexpr type epsilon() { return min(); } + static constexpr type round_error() { return min(); } + static constexpr type infinity() { return min(); } + static constexpr type quiet_NaN() { return min(); } + static constexpr type signaling_NaN() { return min(); } + static constexpr type denorm_min() { return min(); } +}; + #ifdef __clang__ # pragma clang diagnostic pop #endif diff --git a/test/Jamfile b/test/Jamfile index b2bea52..2ec2e0f 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -51,6 +51,7 @@ run quick.cpp ; run test_unsigned_construction.cpp ; compile compile_integer_type_properties.cpp ; +run test_unsigned_narrowing_conversions.cpp ; run test_unsigned_addition.cpp ; compile-fail compile_fail_unsigned_addition.cpp ; @@ -100,6 +101,18 @@ run-fail test_unsigned_strict_mod.cpp ; # Exhaustive verification tests run test_exhaustive_u8_arithmetic.cpp ; run test_boundary_arithmetic.cpp ; +run test_unsigned_bounded_construction.cpp ; +run test_unsigned_bounded_addition.cpp ; +run test_unsigned_bounded_subtraction.cpp ; +run test_unsigned_bounded_multiplication.cpp ; +run test_unsigned_bounded_division.cpp ; +run test_unsigned_bounded_modulo.cpp ; +run test_unsigned_bounded_conversions.cpp ; +run test_unsigned_bounded_charconv.cpp ; +run test_unsigned_bounded_fmt_format.cpp ; +run test_unsigned_bounded_std_format.cpp ; +compile-fail compile_fail_bounded_mixed_ops.cpp ; +compile-fail compile_fail_bounded_bool_bounds.cpp ; # Compile Tests compile compile_tests/compile_test_unsigned_integers.cpp ; @@ -126,3 +139,4 @@ run ../examples/fmt_format.cpp ; run ../examples/iostream.cpp ; run ../examples/charconv.cpp ; run ../examples/bit.cpp ; +run ../examples/bounded_limits.cpp ; diff --git a/test/compile_fail_bounded_bool_bounds.cpp b/test/compile_fail_bounded_bool_bounds.cpp new file mode 100644 index 0000000..67dfbb4 --- /dev/null +++ b/test/compile_fail_bounded_bool_bounds.cpp @@ -0,0 +1,15 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +using namespace boost::safe_numbers; + +int main() +{ + // bool bounds should be rejected + bounded_uint a {u8{0}}; + + return 0; +} diff --git a/test/compile_fail_bounded_mixed_ops.cpp b/test/compile_fail_bounded_mixed_ops.cpp new file mode 100644 index 0000000..47caa3d --- /dev/null +++ b/test/compile_fail_bounded_mixed_ops.cpp @@ -0,0 +1,18 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +using namespace boost::safe_numbers; + +int main() +{ + constexpr bounded_uint<0u, 255u> a {u8{10}}; + constexpr bounded_uint<0u, 100u> b {u8{10}}; + + // This should fail to compile: different bounds + auto c = a + b; + + return 0; +} diff --git a/test/test_bit.cpp b/test/test_bit.cpp index ee1ecab..c937c1f 100644 --- a/test/test_bit.cpp +++ b/test/test_bit.cpp @@ -517,6 +517,121 @@ void test_byteswap_u128() } } +// ----------------------------------------------- +// bounded_uint types covering full underlying range +// ----------------------------------------------- + +using bounded_u8_full = bounded_uint<0u, 255u>; +using bounded_u16_full = bounded_uint<0u, 65535u>; +using bounded_u32_full = bounded_uint<0u, 4294967295u>; +using bounded_u64_full = bounded_uint<0ULL, UINT64_MAX>; + +// ----------------------------------------------- +// Narrower bounded_uint tests +// ----------------------------------------------- + +template +void test_bounded_has_single_bit(UnderlyingT lo, UnderlyingT hi) +{ + boost::random::uniform_int_distribution dist {lo, hi}; + + for (std::size_t i {}; i < N; ++i) + { + const auto raw {dist(rng)}; + const auto expected {std::has_single_bit(raw)}; + const BoundedT wrapped {raw}; + BOOST_TEST_EQ(expected, boost::safe_numbers::has_single_bit(wrapped)); + } +} + +template +void test_bounded_bit_width(UnderlyingT lo, UnderlyingT hi) +{ + boost::random::uniform_int_distribution dist {lo, hi}; + + for (std::size_t i {}; i < N; ++i) + { + const auto raw {dist(rng)}; + const auto expected {std::bit_width(raw)}; + const BoundedT wrapped {raw}; + BOOST_TEST_EQ(expected, boost::safe_numbers::bit_width(wrapped)); + } +} + +template +void test_bounded_popcount(UnderlyingT lo, UnderlyingT hi) +{ + boost::random::uniform_int_distribution dist {lo, hi}; + + for (std::size_t i {}; i < N; ++i) + { + const auto raw {dist(rng)}; + const auto expected {std::popcount(raw)}; + const BoundedT wrapped {raw}; + BOOST_TEST_EQ(expected, boost::safe_numbers::popcount(wrapped)); + } +} + +template +void test_bounded_countl_zero(UnderlyingT lo, UnderlyingT hi) +{ + boost::random::uniform_int_distribution dist {lo, hi}; + + for (std::size_t i {}; i < N; ++i) + { + const auto raw {dist(rng)}; + const auto expected {std::countl_zero(raw)}; + const BoundedT wrapped {raw}; + BOOST_TEST_EQ(expected, boost::safe_numbers::countl_zero(wrapped)); + } +} + +template +void test_bounded_countr_zero(UnderlyingT lo, UnderlyingT hi) +{ + boost::random::uniform_int_distribution dist {lo, hi}; + + for (std::size_t i {}; i < N; ++i) + { + const auto raw {dist(rng)}; + const auto expected {std::countr_zero(raw)}; + const BoundedT wrapped {raw}; + BOOST_TEST_EQ(expected, boost::safe_numbers::countr_zero(wrapped)); + } +} + +void test_narrow_bounded_bit_ops() +{ + using narrow_u8 = bounded_uint<10u, 200u>; + using narrow_u16 = bounded_uint<256u, 40000u>; + using narrow_u32 = bounded_uint<1000u, 100000u>; + + // has_single_bit + test_bounded_has_single_bit(std::uint8_t{10}, std::uint8_t{200}); + test_bounded_has_single_bit(std::uint16_t{256}, std::uint16_t{40000}); + test_bounded_has_single_bit(std::uint32_t{1000}, std::uint32_t{100000}); + + // bit_width + test_bounded_bit_width(std::uint8_t{10}, std::uint8_t{200}); + test_bounded_bit_width(std::uint16_t{256}, std::uint16_t{40000}); + test_bounded_bit_width(std::uint32_t{1000}, std::uint32_t{100000}); + + // popcount + test_bounded_popcount(std::uint8_t{10}, std::uint8_t{200}); + test_bounded_popcount(std::uint16_t{256}, std::uint16_t{40000}); + test_bounded_popcount(std::uint32_t{1000}, std::uint32_t{100000}); + + // countl_zero + test_bounded_countl_zero(std::uint8_t{10}, std::uint8_t{200}); + test_bounded_countl_zero(std::uint16_t{256}, std::uint16_t{40000}); + test_bounded_countl_zero(std::uint32_t{1000}, std::uint32_t{100000}); + + // countr_zero + test_bounded_countr_zero(std::uint8_t{10}, std::uint8_t{200}); + test_bounded_countr_zero(std::uint16_t{256}, std::uint16_t{40000}); + test_bounded_countr_zero(std::uint32_t{1000}, std::uint32_t{100000}); +} + int main() { test_has_single_bit(); @@ -592,5 +707,54 @@ int main() test_byteswap_u128(); + // Full-range bounded_uint tests (reuse existing templates) + test_has_single_bit(); + test_has_single_bit(); + test_has_single_bit(); + test_has_single_bit(); + + test_bit_ceil(); + test_bit_ceil(); + test_bit_ceil(); + test_bit_ceil(); + + test_bit_floor(); + test_bit_floor(); + test_bit_floor(); + test_bit_floor(); + + test_bit_width(); + test_bit_width(); + test_bit_width(); + test_bit_width(); + + test_countl_zero(); + test_countl_zero(); + test_countl_zero(); + test_countl_zero(); + + test_countl_one(); + test_countl_one(); + test_countl_one(); + test_countl_one(); + + test_countr_zero(); + test_countr_zero(); + test_countr_zero(); + test_countr_zero(); + + test_countr_one(); + test_countr_one(); + test_countr_one(); + test_countr_one(); + + test_popcount(); + test_popcount(); + test_popcount(); + test_popcount(); + + // Narrow bounded_uint tests (functions that always return valid results) + test_narrow_bounded_bit_ops(); + return boost::report_errors(); } diff --git a/test/test_limits.cpp b/test/test_limits.cpp index abaaf0d..c0de796 100644 --- a/test/test_limits.cpp +++ b/test/test_limits.cpp @@ -58,6 +58,47 @@ void test() BOOST_TEST_EQ(std::numeric_limits::denorm_min(), T{std::numeric_limits::denorm_min()}); } +template +void test_bounded() +{ + using underlying_type = UnderlyingT; + + // Static properties should match the underlying hardware type + BOOST_TEST_EQ(std::numeric_limits::is_specialized, std::numeric_limits::is_specialized); + BOOST_TEST_EQ(std::numeric_limits::is_signed, std::numeric_limits::is_signed); + BOOST_TEST_EQ(std::numeric_limits::is_integer, std::numeric_limits::is_integer); + BOOST_TEST_EQ(std::numeric_limits::is_exact, std::numeric_limits::is_exact); + BOOST_TEST_EQ(std::numeric_limits::has_infinity, std::numeric_limits::has_infinity); + BOOST_TEST_EQ(std::numeric_limits::has_quiet_NaN, std::numeric_limits::has_quiet_NaN); + BOOST_TEST_EQ(std::numeric_limits::has_signaling_NaN, std::numeric_limits::has_signaling_NaN); + + #if ((!defined(_MSC_VER) && (__cplusplus <= 202002L)) || (defined(_MSC_VER) && (_MSVC_LANG <= 202002L))) + BOOST_TEST(std::numeric_limits::has_denorm == std::numeric_limits::has_denorm); + BOOST_TEST(std::numeric_limits::has_denorm_loss == std::numeric_limits::has_denorm_loss); + #endif + + BOOST_TEST(std::numeric_limits::round_style == std::numeric_limits::round_style); + + BOOST_TEST_EQ(std::numeric_limits::is_iec559, std::numeric_limits::is_iec559); + BOOST_TEST_EQ(std::numeric_limits::is_bounded, std::numeric_limits::is_bounded); + BOOST_TEST_EQ(std::numeric_limits::is_modulo, std::numeric_limits::is_modulo); + BOOST_TEST_EQ(std::numeric_limits::digits, std::numeric_limits::digits); + BOOST_TEST_EQ(std::numeric_limits::digits10, std::numeric_limits::digits10); + BOOST_TEST_EQ(std::numeric_limits::max_digits10, std::numeric_limits::max_digits10); + BOOST_TEST_EQ(std::numeric_limits::radix, std::numeric_limits::radix); + BOOST_TEST_EQ(std::numeric_limits::min_exponent, std::numeric_limits::min_exponent); + BOOST_TEST_EQ(std::numeric_limits::min_exponent10, std::numeric_limits::min_exponent10); + BOOST_TEST_EQ(std::numeric_limits::max_exponent, std::numeric_limits::max_exponent); + BOOST_TEST_EQ(std::numeric_limits::max_exponent10, std::numeric_limits::max_exponent10); + BOOST_TEST_EQ(std::numeric_limits::traps, std::numeric_limits::traps); + BOOST_TEST_EQ(std::numeric_limits::tinyness_before, std::numeric_limits::tinyness_before); + + // min() and max() should return the bounds, not the hardware limits + BOOST_TEST(std::numeric_limits::min() == BoundedT{ExpectedMin}); + BOOST_TEST(std::numeric_limits::max() == BoundedT{ExpectedMax}); + BOOST_TEST(std::numeric_limits::lowest() == std::numeric_limits::min()); +} + int main() { using namespace boost::safe_numbers; @@ -67,5 +108,17 @@ int main() test(); test(); + // Full-range bounded types + test_bounded, std::uint8_t, 0, 255>(); + test_bounded, std::uint16_t, 0, 65535>(); + test_bounded, std::uint32_t, 0, 4294967295u>(); + test_bounded, std::uint64_t, 0, UINT64_MAX>(); + + // Non-zero minimum bounded types + test_bounded, std::uint8_t, 10, 200>(); + test_bounded, std::uint16_t, 256, 40000>(); + test_bounded, std::uint32_t, 1000, 100000>(); + test_bounded, std::uint64_t, 4294967296ULL, UINT64_MAX>(); + return boost::report_errors(); } diff --git a/test/test_unsigned_bounded_addition.cpp b/test/test_unsigned_bounded_addition.cpp new file mode 100644 index 0000000..2693e5a --- /dev/null +++ b/test/test_unsigned_bounded_addition.cpp @@ -0,0 +1,419 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +// 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 + +#include + +#include +#include +#include + +using namespace boost::safe_numbers; +using boost::int128::uint128_t; + +// ----------------------------------------------- +// u8 basis: bounded_uint with Max <= 255 +// ----------------------------------------------- + +void test_u8_valid_addition() +{ + // [0, 255] full range + constexpr bounded_uint<0u, 255u> a {u8{10}}; + constexpr bounded_uint<0u, 255u> b {u8{20}}; + const auto c {a + b}; + const bounded_uint<0u, 255u> expected_c {u8{30}}; + BOOST_TEST(c == expected_c); + + // Adding zeros + constexpr bounded_uint<0u, 255u> zero {u8{0}}; + const auto d {a + zero}; + BOOST_TEST(d == a); + + // Max boundary: 100 + 155 = 255 + constexpr bounded_uint<0u, 255u> e {u8{100}}; + constexpr bounded_uint<0u, 255u> f {u8{155}}; + const auto g {e + f}; + const bounded_uint<0u, 255u> expected_g {u8{255}}; + BOOST_TEST(g == expected_g); + + // Narrower range [10, 200] + constexpr bounded_uint<10u, 200u> h {u8{50}}; + constexpr bounded_uint<10u, 200u> i {u8{30}}; + const auto j {h + i}; + const bounded_uint<10u, 200u> expected_j {u8{80}}; + BOOST_TEST(j == expected_j); + + // Narrower range boundary: 100 + 100 = 200 + constexpr bounded_uint<10u, 200u> k {u8{100}}; + constexpr bounded_uint<10u, 200u> l {u8{100}}; + const auto m {k + l}; + const bounded_uint<10u, 200u> expected_m {u8{200}}; + BOOST_TEST(m == expected_m); +} + +void test_u8_throwing_addition() +{ + // Overflow of underlying u8: 200 + 200 = 400 > 255, u8 addition throws overflow_error + constexpr bounded_uint<0u, 255u> a {u8{200}}; + constexpr bounded_uint<0u, 255u> b {u8{200}}; + BOOST_TEST_THROWS(a + b, std::overflow_error); + + // Result within u8 range but exceeds bounded max: + // [10, 100]: 60 + 60 = 120 > 100, bounded_uint constructor throws domain_error + constexpr bounded_uint<10u, 100u> c {u8{60}}; + constexpr bounded_uint<10u, 100u> d {u8{60}}; + BOOST_TEST_THROWS(c + d, std::domain_error); + + // Just one over the max: 51 + 50 = 101 > 100 + constexpr bounded_uint<10u, 100u> e {u8{51}}; + constexpr bounded_uint<10u, 100u> f {u8{50}}; + BOOST_TEST_THROWS(e + f, std::domain_error); +} + +// ----------------------------------------------- +// u16 basis: bounded_uint with Max in (255, 65535] +// ----------------------------------------------- + +void test_u16_valid_addition() +{ + // [0, 1000] + constexpr bounded_uint<0u, 1000u> a {u16{100}}; + constexpr bounded_uint<0u, 1000u> b {u16{200}}; + const auto c {a + b}; + const bounded_uint<0u, 1000u> expected_c {u16{300}}; + BOOST_TEST(c == expected_c); + + // Adding zeros + constexpr bounded_uint<0u, 1000u> zero {u16{0}}; + const auto d {a + zero}; + BOOST_TEST(d == a); + + // Max boundary: 500 + 500 = 1000 + constexpr bounded_uint<0u, 1000u> e {u16{500}}; + constexpr bounded_uint<0u, 1000u> f {u16{500}}; + const auto g {e + f}; + const bounded_uint<0u, 1000u> expected_g {u16{1000}}; + BOOST_TEST(g == expected_g); + + // [256, 65535] full u16 range with non-zero min + constexpr bounded_uint<256u, 65535u> h {u16{1000}}; + constexpr bounded_uint<256u, 65535u> i {u16{2000}}; + const auto j {h + i}; + const bounded_uint<256u, 65535u> expected_j {u16{3000}}; + BOOST_TEST(j == expected_j); + + // [0, 40000] + constexpr bounded_uint<0u, 40000u> k {u16{10000}}; + constexpr bounded_uint<0u, 40000u> l {u16{20000}}; + const auto m {k + l}; + const bounded_uint<0u, 40000u> expected_m {u16{30000}}; + BOOST_TEST(m == expected_m); +} + +void test_u16_throwing_addition() +{ + // Overflow of underlying u16: 40000 + 40000 = 80000 > 65535 + constexpr bounded_uint<0u, 65535u> a {u16{40000}}; + constexpr bounded_uint<0u, 65535u> b {u16{40000}}; + BOOST_TEST_THROWS(a + b, std::overflow_error); + + // Result within u16 range but exceeds bounded max: + // [0, 1000]: 600 + 600 = 1200 > 1000 + constexpr bounded_uint<0u, 1000u> c {u16{600}}; + constexpr bounded_uint<0u, 1000u> d {u16{600}}; + BOOST_TEST_THROWS(c + d, std::domain_error); + + // Just one over: 501 + 500 = 1001 > 1000 + constexpr bounded_uint<0u, 1000u> e {u16{501}}; + constexpr bounded_uint<0u, 1000u> f {u16{500}}; + BOOST_TEST_THROWS(e + f, std::domain_error); +} + +// ----------------------------------------------- +// u32 basis: bounded_uint with Max in (65535, 4294967295] +// ----------------------------------------------- + +void test_u32_valid_addition() +{ + // [0, 100000] + constexpr bounded_uint<0u, 100000u> a {u32{25000}}; + constexpr bounded_uint<0u, 100000u> b {u32{30000}}; + const auto c {a + b}; + const bounded_uint<0u, 100000u> expected_c {u32{55000}}; + BOOST_TEST(c == expected_c); + + // Adding zeros + constexpr bounded_uint<0u, 100000u> zero {u32{0}}; + const auto d {a + zero}; + BOOST_TEST(d == a); + + // Max boundary: 50000 + 50000 = 100000 + constexpr bounded_uint<0u, 100000u> e {u32{50000}}; + constexpr bounded_uint<0u, 100000u> f {u32{50000}}; + const auto g {e + f}; + const bounded_uint<0u, 100000u> expected_g {u32{100000}}; + BOOST_TEST(g == expected_g); + + // [65536, 4294967295] non-zero min with full u32 max + constexpr bounded_uint<65536u, 4294967295u> h {u32{1000000}}; + constexpr bounded_uint<65536u, 4294967295u> i {u32{2000000}}; + const auto j {h + i}; + const bounded_uint<65536u, 4294967295u> expected_j {u32{3000000}}; + BOOST_TEST(j == expected_j); +} + +void test_u32_throwing_addition() +{ + // Overflow of underlying u32 + constexpr bounded_uint<0u, 4294967295u> a {u32{3000000000u}}; + constexpr bounded_uint<0u, 4294967295u> b {u32{3000000000u}}; + BOOST_TEST_THROWS(a + b, std::overflow_error); + + // Result within u32 range but exceeds bounded max: + // [0, 100000]: 60000 + 60000 = 120000 > 100000 + constexpr bounded_uint<0u, 100000u> c {u32{60000}}; + constexpr bounded_uint<0u, 100000u> d {u32{60000}}; + BOOST_TEST_THROWS(c + d, std::domain_error); + + // Just one over: 50001 + 50000 = 100001 > 100000 + constexpr bounded_uint<0u, 100000u> e {u32{50001}}; + constexpr bounded_uint<0u, 100000u> f {u32{50000}}; + BOOST_TEST_THROWS(e + f, std::domain_error); +} + +// ----------------------------------------------- +// u64 basis: bounded_uint with Max in (4294967295, UINT64_MAX] +// ----------------------------------------------- + +void test_u64_valid_addition() +{ + // [0, 5'000'000'000] + constexpr bounded_uint<0ULL, 5'000'000'000ULL> a {u64{1'000'000'000ULL}}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> b {u64{2'000'000'000ULL}}; + const auto c {a + b}; + const bounded_uint<0ULL, 5'000'000'000ULL> expected_c {u64{3'000'000'000ULL}}; + BOOST_TEST(c == expected_c); + + // Adding zeros + constexpr bounded_uint<0ULL, 5'000'000'000ULL> zero {u64{0}}; + const auto d {a + zero}; + BOOST_TEST(d == a); + + // Max boundary: 2'500'000'000 + 2'500'000'000 = 5'000'000'000 + constexpr bounded_uint<0ULL, 5'000'000'000ULL> e {u64{2'500'000'000ULL}}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> f {u64{2'500'000'000ULL}}; + const auto g {e + f}; + const bounded_uint<0ULL, 5'000'000'000ULL> expected_g {u64{5'000'000'000ULL}}; + BOOST_TEST(g == expected_g); + + // [4294967296, UINT64_MAX] non-zero min + constexpr bounded_uint<4294967296ULL, UINT64_MAX> h {u64{5'000'000'000ULL}}; + constexpr bounded_uint<4294967296ULL, UINT64_MAX> i {u64{6'000'000'000ULL}}; + const auto j {h + i}; + const bounded_uint<4294967296ULL, UINT64_MAX> expected_j {u64{11'000'000'000ULL}}; + BOOST_TEST(j == expected_j); +} + +void test_u64_throwing_addition() +{ + // Overflow of underlying u64 + constexpr bounded_uint<0ULL, UINT64_MAX> a {u64{UINT64_MAX}}; + constexpr bounded_uint<0ULL, UINT64_MAX> b {u64{1}}; + BOOST_TEST_THROWS(a + b, std::overflow_error); + + // Result within u64 range but exceeds bounded max: + // [0, 5'000'000'000]: 3'000'000'000 + 3'000'000'000 = 6'000'000'000 > 5'000'000'000 + constexpr bounded_uint<0ULL, 5'000'000'000ULL> c {u64{3'000'000'000ULL}}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> d {u64{3'000'000'000ULL}}; + BOOST_TEST_THROWS(c + d, std::domain_error); + + // Just one over + constexpr bounded_uint<0ULL, 5'000'000'000ULL> e {u64{2'500'000'001ULL}}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> f {u64{2'500'000'000ULL}}; + BOOST_TEST_THROWS(e + f, std::domain_error); +} + +// ----------------------------------------------- +// u128 basis: bounded_uint with Max > UINT64_MAX +// ----------------------------------------------- + +void test_u128_valid_addition() +{ + // [0, 2^64] range + constexpr auto max_val = uint128_t{1, 0}; + using b128 = bounded_uint; + + const b128 a {u128{uint128_t{100}}}; + const b128 b {u128{uint128_t{200}}}; + const auto c {a + b}; + const b128 expected_c {u128{uint128_t{300}}}; + BOOST_TEST(c == expected_c); + + // Adding zeros + const b128 zero {u128{uint128_t{0}}}; + const auto d {a + zero}; + BOOST_TEST(d == a); + + // Larger values: UINT64_MAX + 1 = 2^64 + const b128 e {u128{uint128_t{UINT64_MAX}}}; + const b128 f {u128{uint128_t{1}}}; + const auto g {e + f}; + const b128 expected_g {u128{uint128_t{1, 0}}}; + BOOST_TEST(g == expected_g); + + // Full u128 range + constexpr auto u128_max = uint128_t{UINT64_MAX, UINT64_MAX}; + using b128_full = bounded_uint; + + const b128_full h {u128{uint128_t{1'000'000}}}; + const b128_full i {u128{uint128_t{2'000'000}}}; + const auto j {h + i}; + const b128_full expected_j {u128{uint128_t{3'000'000}}}; + BOOST_TEST(j == expected_j); +} + +void test_u128_throwing_addition() +{ + // Result exceeds bounded max: + // [0, 2^64]: max_val + 1 > max_val + constexpr auto max_val = uint128_t{1, 0}; + using b128 = bounded_uint; + + const b128 a {u128{max_val}}; + const b128 b {u128{uint128_t{1}}}; + BOOST_TEST_THROWS(a + b, std::domain_error); + + // Non-zero min range: [1000, 2^64] + constexpr auto min_val = uint128_t{1000}; + using b128_ranged = bounded_uint; + + const b128_ranged c {u128{max_val}}; + const b128_ranged d {u128{uint128_t{1000}}}; + BOOST_TEST_THROWS(c + d, std::domain_error); +} + +// ----------------------------------------------- +// Constexpr addition tests +// ----------------------------------------------- + +void test_constexpr_addition() +{ + // u8 basis + constexpr bounded_uint<0u, 255u> a {u8{10}}; + constexpr bounded_uint<0u, 255u> b {u8{20}}; + constexpr auto c {a + b}; + constexpr bounded_uint<0u, 255u> expected_c {u8{30}}; + static_assert(c == expected_c); + + // u16 basis + constexpr bounded_uint<0u, 1000u> d {u16{100}}; + constexpr bounded_uint<0u, 1000u> e {u16{200}}; + constexpr auto f {d + e}; + constexpr bounded_uint<0u, 1000u> expected_f {u16{300}}; + static_assert(f == expected_f); + + // u32 basis + constexpr bounded_uint<0u, 100000u> g {u32{25000}}; + constexpr bounded_uint<0u, 100000u> h {u32{30000}}; + constexpr auto i {g + h}; + constexpr bounded_uint<0u, 100000u> expected_i {u32{55000}}; + static_assert(i == expected_i); + + // u64 basis + constexpr bounded_uint<0ULL, 5'000'000'000ULL> j {u64{1'000'000'000ULL}}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> k {u64{2'000'000'000ULL}}; + constexpr auto l {j + k}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> expected_l {u64{3'000'000'000ULL}}; + static_assert(l == expected_l); +} + +// ----------------------------------------------- +// Various bound configurations +// ----------------------------------------------- + +void test_tight_bounds() +{ + // Very narrow range: [50, 60] + constexpr bounded_uint<50u, 60u> a {u8{50}}; + constexpr bounded_uint<50u, 60u> b {u8{50}}; + + // 50 + 50 = 100 which exceeds [50, 60] + BOOST_TEST_THROWS(a + b, std::domain_error); + + // [100, 200] + constexpr bounded_uint<100u, 200u> c {u8{100}}; + constexpr bounded_uint<100u, 200u> d {u8{100}}; + const auto e {c + d}; + const bounded_uint<100u, 200u> expected_e {u8{200}}; + BOOST_TEST(e == expected_e); + + // [100, 200]: 101 + 100 = 201 > 200 + constexpr bounded_uint<100u, 200u> f {u8{101}}; + constexpr bounded_uint<100u, 200u> g {u8{100}}; + BOOST_TEST_THROWS(f + g, std::domain_error); +} + +void test_power_of_two_bounds() +{ + // [0, 128] - not a full byte + constexpr bounded_uint<0u, 128u> a {u8{64}}; + constexpr bounded_uint<0u, 128u> b {u8{64}}; + const auto c {a + b}; + const bounded_uint<0u, 128u> expected_c {u8{128}}; + BOOST_TEST(c == expected_c); + + // 65 + 64 = 129 > 128 + constexpr bounded_uint<0u, 128u> d {u8{65}}; + constexpr bounded_uint<0u, 128u> e {u8{64}}; + BOOST_TEST_THROWS(d + e, std::domain_error); + + // [0, 1024] + constexpr bounded_uint<0u, 1024u> f {u16{512}}; + constexpr bounded_uint<0u, 1024u> g {u16{512}}; + const auto h {f + g}; + const bounded_uint<0u, 1024u> expected_h {u16{1024}}; + BOOST_TEST(h == expected_h); + + // 513 + 512 = 1025 > 1024 + constexpr bounded_uint<0u, 1024u> i {u16{513}}; + constexpr bounded_uint<0u, 1024u> j {u16{512}}; + BOOST_TEST_THROWS(i + j, std::domain_error); +} + +int main() +{ + test_u8_valid_addition(); + test_u8_throwing_addition(); + + test_u16_valid_addition(); + test_u16_throwing_addition(); + + test_u32_valid_addition(); + test_u32_throwing_addition(); + + test_u64_valid_addition(); + test_u64_throwing_addition(); + + test_u128_valid_addition(); + test_u128_throwing_addition(); + + test_constexpr_addition(); + test_tight_bounds(); + test_power_of_two_bounds(); + + return boost::report_errors(); +} diff --git a/test/test_unsigned_bounded_charconv.cpp b/test/test_unsigned_bounded_charconv.cpp new file mode 100644 index 0000000..a4433d1 --- /dev/null +++ b/test/test_unsigned_bounded_charconv.cpp @@ -0,0 +1,388 @@ +// 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 test 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 +#include +#include + +#endif + +using namespace boost::safe_numbers; + +static std::mt19937_64 rng {42}; + +// bounded_uint has no default constructor, so use a copy of value as placeholder +template +void test(T value, int base = 10) +{ + char buffer[256]; + const auto r_to {to_chars(buffer, buffer + sizeof(buffer), value, base)}; + BOOST_TEST(r_to.ec == std::errc{}); + + auto return_val {value}; + const auto r_from {from_chars(buffer, r_to.ptr, return_val, base)}; + BOOST_TEST(r_from.ec == std::errc{}); + + BOOST_TEST_EQ(value, return_val); +} + +// ----------------------------------------------- +// Roundtrip tests for u8-backed bounded_uint +// ----------------------------------------------- + +void test_u8_range_roundtrip() +{ + using type = bounded_uint<0u, 200u>; + + // Boundary values + test(type{u8{0}}); + test(type{u8{200}}); + + // Mid-range + test(type{u8{1}}); + test(type{u8{100}}); + test(type{u8{150}}); + + // Random values within bounds + boost::random::uniform_int_distribution dist {0, 200}; + for (int i {}; i < 256; ++i) + { + test(type{u8{dist(rng)}}); + } +} + +void test_u8_nonzero_min_roundtrip() +{ + using type = bounded_uint<10u, 100u>; + + // Boundary values + test(type{u8{10}}); + test(type{u8{100}}); + + // Mid-range + test(type{u8{50}}); + test(type{u8{75}}); + + // Random values within bounds + boost::random::uniform_int_distribution dist {10, 100}; + for (int i {}; i < 256; ++i) + { + test(type{u8{dist(rng)}}); + } +} + +// ----------------------------------------------- +// Roundtrip tests for u16-backed bounded_uint +// ----------------------------------------------- + +void test_u16_range_roundtrip() +{ + using type = bounded_uint<0u, 40000u>; + + // Boundary values + test(type{u16{0}}); + test(type{u16{40000}}); + + // Mid-range + test(type{u16{1}}); + test(type{u16{20000}}); + + // Random values within bounds + boost::random::uniform_int_distribution dist {0, 40000}; + for (int i {}; i < 1024; ++i) + { + test(type{u16{dist(rng)}}); + } +} + +void test_u16_nonzero_min_roundtrip() +{ + using type = bounded_uint<256u, 65535u>; + + // Boundary values + test(type{u16{256}}); + test(type{u16{65535}}); + + // Mid-range + test(type{u16{1000}}); + test(type{u16{30000}}); + + // Random values within bounds + boost::random::uniform_int_distribution dist {256, 65535}; + for (int i {}; i < 1024; ++i) + { + test(type{u16{dist(rng)}}); + } +} + +// ----------------------------------------------- +// Roundtrip tests for u32-backed bounded_uint +// ----------------------------------------------- + +void test_u32_range_roundtrip() +{ + using type = bounded_uint<0u, 100000u>; + + // Boundary values + test(type{u32{0}}); + test(type{u32{100000}}); + + // Mid-range + test(type{u32{1}}); + test(type{u32{50000}}); + + // Random values within bounds + boost::random::uniform_int_distribution dist {0, 100000}; + for (int i {}; i < 1024; ++i) + { + test(type{u32{dist(rng)}}); + } +} + +void test_u32_nonzero_min_roundtrip() +{ + using type = bounded_uint<65536u, 4294967295u>; + + // Boundary values + test(type{u32{65536}}); + test(type{u32{4294967295u}}); + + // Mid-range + test(type{u32{1000000}}); + test(type{u32{2000000000u}}); + + // Random values within bounds + boost::random::uniform_int_distribution dist {65536, 4294967295u}; + for (int i {}; i < 1024; ++i) + { + test(type{u32{dist(rng)}}); + } +} + +// ----------------------------------------------- +// Roundtrip tests for u64-backed bounded_uint +// ----------------------------------------------- + +void test_u64_range_roundtrip() +{ + using type = bounded_uint<0ULL, 5'000'000'000ULL>; + + // Boundary values + test(type{u64{0}}); + test(type{u64{5'000'000'000ULL}}); + + // Mid-range + test(type{u64{1}}); + test(type{u64{2'500'000'000ULL}}); + + // Random values within bounds + boost::random::uniform_int_distribution dist {0, 5'000'000'000ULL}; + for (int i {}; i < 1024; ++i) + { + test(type{u64{dist(rng)}}); + } +} + +void test_u64_nonzero_min_roundtrip() +{ + using type = bounded_uint<4294967296ULL, UINT64_MAX>; + + // Boundary values + test(type{u64{4294967296ULL}}); + test(type{u64{UINT64_MAX}}); + + // Mid-range + test(type{u64{5'000'000'000ULL}}); + test(type{u64{UINT64_MAX / 2}}); + + // Random values within bounds + boost::random::uniform_int_distribution dist {4294967296ULL, UINT64_MAX}; + for (int i {}; i < 1024; ++i) + { + test(type{u64{dist(rng)}}); + } +} + +// ----------------------------------------------- +// Multi-base roundtrip tests +// ----------------------------------------------- + +void test_multiple_bases() +{ + using type = bounded_uint<0u, 200u>; + + for (int base {2}; base <= 36; ++base) + { + test(type{u8{0}}, base); + test(type{u8{1}}, base); + test(type{u8{100}}, base); + test(type{u8{200}}, base); + } + + using type16 = bounded_uint<0u, 40000u>; + + for (int base {2}; base <= 36; ++base) + { + test(type16{u16{0}}, base); + test(type16{u16{1000}}, base); + test(type16{u16{40000}}, base); + } + + using type32 = bounded_uint<0u, 100000u>; + + for (int base {2}; base <= 36; ++base) + { + test(type32{u32{0}}, base); + test(type32{u32{50000}}, base); + test(type32{u32{100000}}, base); + } + + using type64 = bounded_uint<0ULL, 5'000'000'000ULL>; + + for (int base {2}; base <= 36; ++base) + { + test(type64{u64{0}}, base); + test(type64{u64{2'500'000'000ULL}}, base); + test(type64{u64{5'000'000'000ULL}}, base); + } +} + +// ----------------------------------------------- +// from_chars with out-of-bounds values +// ----------------------------------------------- + +void test_from_chars_out_of_bounds() +{ + // Value above maximum should throw during from_chars + { + using type = bounded_uint<0u, 100u>; + const char str[] = "200"; + auto val {type{u8{50}}}; + BOOST_TEST_THROWS(from_chars(str, str + std::strlen(str), val), std::domain_error); + } + + // Value below minimum should throw during from_chars + { + using type = bounded_uint<10u, 200u>; + const char str[] = "5"; + auto val {type{u8{50}}}; + BOOST_TEST_THROWS(from_chars(str, str + std::strlen(str), val), std::domain_error); + } + + // Value of zero below non-zero minimum + { + using type = bounded_uint<10u, 200u>; + const char str[] = "0"; + auto val {type{u8{50}}}; + BOOST_TEST_THROWS(from_chars(str, str + std::strlen(str), val), std::domain_error); + } + + // u16 range: value above max + { + using type = bounded_uint<0u, 1000u>; + const char str[] = "1001"; + auto val {type{u16{500}}}; + BOOST_TEST_THROWS(from_chars(str, str + std::strlen(str), val), std::domain_error); + } + + // u16 range: value below min + { + using type = bounded_uint<500u, 1000u>; + const char str[] = "499"; + auto val {type{u16{500}}}; + BOOST_TEST_THROWS(from_chars(str, str + std::strlen(str), val), std::domain_error); + } + + // u32 range: value above max + { + using type = bounded_uint<0u, 100000u>; + const char str[] = "100001"; + auto val {type{u32{50000}}}; + BOOST_TEST_THROWS(from_chars(str, str + std::strlen(str), val), std::domain_error); + } +} + +int main() +{ + test_u8_range_roundtrip(); + test_u8_nonzero_min_roundtrip(); + test_u16_range_roundtrip(); + test_u16_nonzero_min_roundtrip(); + test_u32_range_roundtrip(); + test_u32_nonzero_min_roundtrip(); + test_u64_range_roundtrip(); + test_u64_nonzero_min_roundtrip(); + test_multiple_bases(); + test_from_chars_out_of_bounds(); + + return boost::report_errors(); +} diff --git a/test/test_unsigned_bounded_construction.cpp b/test/test_unsigned_bounded_construction.cpp new file mode 100644 index 0000000..8537f87 --- /dev/null +++ b/test/test_unsigned_bounded_construction.cpp @@ -0,0 +1,341 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include +#include + +#include +#include +#include +#include + +using namespace boost::safe_numbers; +using boost::int128::uint128_t; + +// ----------------------------------------------- +// Static assertions on basis_type selection +// ----------------------------------------------- + +// Max fits in uint8_t -> basis_type should be u8 +static_assert(std::is_same_v::basis_type, u8>); +static_assert(std::is_same_v::basis_type, u8>); +static_assert(std::is_same_v::basis_type, u8>); + +// Max exceeds uint8_t but fits in uint16_t -> basis_type should be u16 +static_assert(std::is_same_v::basis_type, u16>); +static_assert(std::is_same_v::basis_type, u16>); +static_assert(std::is_same_v::basis_type, u16>); + +// Max exceeds uint16_t but fits in uint32_t -> basis_type should be u32 +static_assert(std::is_same_v::basis_type, u32>); +static_assert(std::is_same_v::basis_type, u32>); +static_assert(std::is_same_v::basis_type, u32>); + +// Max exceeds uint32_t but fits in uint64_t -> basis_type should be u64 +static_assert(std::is_same_v::basis_type, u64>); +static_assert(std::is_same_v::basis_type, u64>); +static_assert(std::is_same_v::basis_type, u64>); + +// Max exceeds uint64_t -> basis_type should be u128 +static_assert(std::is_same_v::basis_type, u128>); +static_assert(std::is_same_v::basis_type, u128>); + +// ----------------------------------------------- +// sizeof checks - bounded_uint should be the same +// size as the underlying safe type +// ----------------------------------------------- + +static_assert(sizeof(bounded_uint<0u, 200u>) == sizeof(u8)); +static_assert(sizeof(bounded_uint<0u, 40000u>) == sizeof(u16)); +static_assert(sizeof(bounded_uint<0u, 100000u>) == sizeof(u32)); +static_assert(sizeof(bounded_uint<0ULL, 5'000'000'000ULL>) == sizeof(u64)); +static_assert(sizeof(bounded_uint) == sizeof(u128)); + +// ----------------------------------------------- +// Construction and comparison tests +// ----------------------------------------------- + +void test_u8_range() +{ + // Construction from value + constexpr bounded_uint<0u, 255u> a {u8{0}}; + constexpr bounded_uint<0u, 255u> b {u8{0}}; + BOOST_TEST(a == b); + + constexpr bounded_uint<0u, 255u> c {u8{42}}; + constexpr bounded_uint<0u, 255u> d {u8{42}}; + BOOST_TEST(c == d); + BOOST_TEST(c != a); + + constexpr bounded_uint<0u, 255u> e {u8{255}}; + BOOST_TEST(e > c); + BOOST_TEST(c < e); + BOOST_TEST(a <= c); + BOOST_TEST(e >= c); + + // Non-zero minimum + constexpr bounded_uint<10u, 200u> f {u8{10}}; + constexpr bounded_uint<10u, 200u> g {u8{200}}; + constexpr bounded_uint<10u, 200u> h {u8{100}}; + BOOST_TEST(f < g); + BOOST_TEST(h > f); + BOOST_TEST(h < g); +} + +void test_u16_range() +{ + constexpr bounded_uint<0u, 1000u> a {u16{0}}; + constexpr bounded_uint<0u, 1000u> b {u16{0}}; + BOOST_TEST(a == b); + + constexpr bounded_uint<0u, 1000u> c {u16{500}}; + constexpr bounded_uint<0u, 1000u> d {u16{1000}}; + BOOST_TEST(c < d); + BOOST_TEST(c != a); + + constexpr bounded_uint<256u, 65535u> e {u16{256}}; + constexpr bounded_uint<256u, 65535u> f {u16{65535}}; + constexpr bounded_uint<256u, 65535u> g {u16{30000}}; + BOOST_TEST(e < f); + BOOST_TEST(g > e); + BOOST_TEST(g < f); +} + +void test_u32_range() +{ + constexpr bounded_uint<0u, 100000u> a {u32{0}}; + constexpr bounded_uint<0u, 100000u> b {u32{100000}}; + constexpr bounded_uint<0u, 100000u> c {u32{50000}}; + BOOST_TEST(a < b); + BOOST_TEST(c > a); + BOOST_TEST(c < b); + + constexpr bounded_uint<65536u, 4294967295u> d {u32{65536}}; + constexpr bounded_uint<65536u, 4294967295u> e {u32{4294967295u}}; + constexpr bounded_uint<65536u, 4294967295u> f {u32{1000000}}; + BOOST_TEST(d < e); + BOOST_TEST(f > d); + BOOST_TEST(f < e); +} + +void test_u64_range() +{ + constexpr bounded_uint<0ULL, 5'000'000'000ULL> a {u64{0}}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> b {u64{5'000'000'000ULL}}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> c {u64{2'500'000'000ULL}}; + BOOST_TEST(a < b); + BOOST_TEST(c > a); + BOOST_TEST(c < b); + + constexpr bounded_uint<4294967296ULL, UINT64_MAX> d {u64{4294967296ULL}}; + constexpr bounded_uint<4294967296ULL, UINT64_MAX> e {u64{UINT64_MAX}}; + BOOST_TEST(d < e); + BOOST_TEST(d == d); + BOOST_TEST(e == e); +} + +void test_u128_range() +{ + // Min = 0, Max = 2^64 (one beyond uint64_t max) + constexpr auto max_val = uint128_t{1, 0}; + using b128 = bounded_uint; + + constexpr b128 a {u128{uint128_t{0}}}; + constexpr b128 b {u128{uint128_t{42}}}; + constexpr b128 c {u128{uint128_t{1000}}}; + BOOST_TEST(a < b); + BOOST_TEST(b < c); + BOOST_TEST(a < c); + BOOST_TEST(a == a); + BOOST_TEST(b == b); + + // Max = uint128_t maximum, test values spanning both halves + constexpr auto u128_max = uint128_t{UINT64_MAX, UINT64_MAX}; + using b128_full = bounded_uint; + + constexpr b128_full d {u128{uint128_t{0}}}; + constexpr b128_full e {u128{uint128_t{1'000'000}}}; + constexpr b128_full f {u128{uint128_t{1, 0}}}; + constexpr b128_full g {u128{u128_max}}; + BOOST_TEST(d < e); + BOOST_TEST(e < f); + BOOST_TEST(f < g); + BOOST_TEST(d < g); +} + +void test_same_value_equality() +{ + // Verify that constructing the same value yields equal objects across all widths + constexpr bounded_uint<0u, 200u> a8 {u8{100}}; + constexpr bounded_uint<0u, 200u> b8 {u8{100}}; + BOOST_TEST(a8 == b8); + BOOST_TEST(!(a8 != b8)); + BOOST_TEST(!(a8 < b8)); + BOOST_TEST(!(a8 > b8)); + BOOST_TEST(a8 <= b8); + BOOST_TEST(a8 >= b8); + + constexpr bounded_uint<0u, 1000u> a16 {u16{500}}; + constexpr bounded_uint<0u, 1000u> b16 {u16{500}}; + BOOST_TEST(a16 == b16); + + constexpr bounded_uint<0u, 100000u> a32 {u32{50000}}; + constexpr bounded_uint<0u, 100000u> b32 {u32{50000}}; + BOOST_TEST(a32 == b32); + + constexpr bounded_uint<0ULL, 5'000'000'000ULL> a64 {u64{2'500'000'000ULL}}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> b64 {u64{2'500'000'000ULL}}; + BOOST_TEST(a64 == b64); +} + +// ----------------------------------------------- +// Bounds-checking exception tests +// ----------------------------------------------- + +void test_u8_bounds_checking() +{ + // Exact boundary values should NOT throw + bounded_uint<10u, 200u> a {u8{10}}; + bounded_uint<10u, 200u> b {u8{200}}; + (void)a; + (void)b; + + // Below minimum should throw + BOOST_TEST_THROWS((bounded_uint<10u, 200u>{u8{9}}), std::domain_error); + BOOST_TEST_THROWS((bounded_uint<10u, 200u>{u8{0}}), std::domain_error); + + // Above maximum should throw + BOOST_TEST_THROWS((bounded_uint<10u, 200u>{u8{201}}), std::domain_error); + BOOST_TEST_THROWS((bounded_uint<10u, 200u>{u8{255}}), std::domain_error); + + // Single-value above max for full-range lower bound + BOOST_TEST_THROWS((bounded_uint<0u, 100u>{u8{101}}), std::domain_error); + BOOST_TEST_THROWS((bounded_uint<0u, 100u>{u8{255}}), std::domain_error); +} + +void test_u16_bounds_checking() +{ + // Exact boundary values should NOT throw + bounded_uint<256u, 65535u> a {u16{256}}; + bounded_uint<256u, 65535u> b {u16{65535}}; + (void)a; + (void)b; + + // Below minimum should throw + BOOST_TEST_THROWS((bounded_uint<256u, 65535u>{u16{255}}), std::domain_error); + BOOST_TEST_THROWS((bounded_uint<256u, 65535u>{u16{0}}), std::domain_error); + + // Above maximum for narrower range + BOOST_TEST_THROWS((bounded_uint<0u, 1000u>{u16{1001}}), std::domain_error); + BOOST_TEST_THROWS((bounded_uint<0u, 1000u>{u16{65535}}), std::domain_error); + + // Below minimum for narrower range + BOOST_TEST_THROWS((bounded_uint<500u, 1000u>{u16{499}}), std::domain_error); + BOOST_TEST_THROWS((bounded_uint<500u, 1000u>{u16{0}}), std::domain_error); +} + +void test_u32_bounds_checking() +{ + // Exact boundary values should NOT throw + bounded_uint<65536u, 4294967295u> a {u32{65536}}; + bounded_uint<65536u, 4294967295u> b {u32{4294967295u}}; + (void)a; + (void)b; + + // Below minimum should throw + BOOST_TEST_THROWS((bounded_uint<65536u, 4294967295u>{u32{65535}}), std::domain_error); + BOOST_TEST_THROWS((bounded_uint<65536u, 4294967295u>{u32{0}}), std::domain_error); + + // Above maximum for narrower range + BOOST_TEST_THROWS((bounded_uint<0u, 100000u>{u32{100001}}), std::domain_error); + BOOST_TEST_THROWS((bounded_uint<0u, 100000u>{u32{4294967295u}}), std::domain_error); +} + +void test_u64_bounds_checking() +{ + // Exact boundary values should NOT throw + bounded_uint<4294967296ULL, UINT64_MAX> a {u64{4294967296ULL}}; + bounded_uint<4294967296ULL, UINT64_MAX> b {u64{UINT64_MAX}}; + (void)a; + (void)b; + + // Below minimum should throw + BOOST_TEST_THROWS((bounded_uint<4294967296ULL, UINT64_MAX>{u64{4294967295ULL}}), std::domain_error); + BOOST_TEST_THROWS((bounded_uint<4294967296ULL, UINT64_MAX>{u64{0}}), std::domain_error); + + // Above maximum for narrower range + BOOST_TEST_THROWS((bounded_uint<0ULL, 5'000'000'000ULL>{u64{5'000'000'001ULL}}), std::domain_error); + BOOST_TEST_THROWS((bounded_uint<0ULL, 5'000'000'000ULL>{u64{UINT64_MAX}}), std::domain_error); +} + +void test_u128_bounds_checking() +{ + // Exact boundary values should NOT throw + constexpr auto max_val = uint128_t{1, 0}; + using b128 = bounded_uint; + + b128 a {u128{uint128_t{0}}}; + b128 b {u128{max_val}}; + (void)a; + (void)b; + + // Above maximum should throw + constexpr auto above_max = uint128_t{1, 1}; + BOOST_TEST_THROWS((b128{u128{above_max}}), std::domain_error); + + // Non-zero minimum range + constexpr auto min_val = uint128_t{1000}; + constexpr auto max_val2 = uint128_t{1, 0}; + using b128_ranged = bounded_uint; + + b128_ranged c {u128{uint128_t{1000}}}; + b128_ranged d {u128{max_val2}}; + (void)c; + (void)d; + + // Below minimum should throw + BOOST_TEST_THROWS((b128_ranged{u128{uint128_t{999}}}), std::domain_error); + BOOST_TEST_THROWS((b128_ranged{u128{uint128_t{0}}}), std::domain_error); +} + +void test_constexpr_construction() +{ + // Verify all constructions are valid in constexpr context + constexpr bounded_uint<0u, 255u> a {u8{0}}; + constexpr bounded_uint<0u, 255u> b {u8{255}}; + static_assert(a < b); + static_assert(a == a); + static_assert(b == b); + static_assert(a != b); + + constexpr bounded_uint<0u, 65535u> c {u16{0}}; + constexpr bounded_uint<0u, 65535u> d {u16{65535}}; + static_assert(c < d); + + constexpr bounded_uint<0u, 4294967295u> e {u32{0}}; + constexpr bounded_uint<0u, 4294967295u> f {u32{4294967295u}}; + static_assert(e < f); + + constexpr bounded_uint<0ULL, UINT64_MAX> g {u64{0}}; + constexpr bounded_uint<0ULL, UINT64_MAX> h {u64{UINT64_MAX}}; + static_assert(g < h); +} + +int main() +{ + test_u8_range(); + test_u16_range(); + test_u32_range(); + test_u64_range(); + test_u128_range(); + test_same_value_equality(); + test_constexpr_construction(); + test_u8_bounds_checking(); + test_u16_bounds_checking(); + test_u32_bounds_checking(); + test_u64_bounds_checking(); + test_u128_bounds_checking(); + + return boost::report_errors(); +} diff --git a/test/test_unsigned_bounded_conversions.cpp b/test/test_unsigned_bounded_conversions.cpp new file mode 100644 index 0000000..881d231 --- /dev/null +++ b/test/test_unsigned_bounded_conversions.cpp @@ -0,0 +1,529 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +// 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 + +#include + +#include +#include +#include + +using namespace boost::safe_numbers; +using boost::int128::uint128_t; + +// ----------------------------------------------- +// Same basis type, different bounds +// ----------------------------------------------- + +void test_u8_to_u8_wider() +{ + // [10, 100] -> [0, 255]: value 50 fits in wider range + constexpr bounded_uint<10u, 100u> a {u8{50}}; + const auto b {static_cast>(a)}; + const bounded_uint<0u, 255u> expected {u8{50}}; + BOOST_TEST(b == expected); + + // Min boundary: [10, 100] -> [0, 255]: value 10 + constexpr bounded_uint<10u, 100u> c {u8{10}}; + const auto d {static_cast>(c)}; + const bounded_uint<0u, 255u> expected_d {u8{10}}; + BOOST_TEST(d == expected_d); + + // Max boundary: [10, 100] -> [0, 255]: value 100 + constexpr bounded_uint<10u, 100u> e {u8{100}}; + const auto f {static_cast>(e)}; + const bounded_uint<0u, 255u> expected_f {u8{100}}; + BOOST_TEST(f == expected_f); +} + +void test_u8_to_u8_narrower() +{ + // [0, 255] -> [10, 100]: value 50 fits in narrower range + constexpr bounded_uint<0u, 255u> a {u8{50}}; + const auto b {static_cast>(a)}; + const bounded_uint<10u, 100u> expected {u8{50}}; + BOOST_TEST(b == expected); + + // Exact boundaries of target + constexpr bounded_uint<0u, 255u> c {u8{10}}; + const auto d {static_cast>(c)}; + const bounded_uint<10u, 100u> expected_d {u8{10}}; + BOOST_TEST(d == expected_d); + + constexpr bounded_uint<0u, 255u> e {u8{100}}; + const auto f {static_cast>(e)}; + const bounded_uint<10u, 100u> expected_f {u8{100}}; + BOOST_TEST(f == expected_f); +} + +void test_u8_to_u8_throwing() +{ + // [0, 255] -> [10, 100]: value 5 < 10, below target min + constexpr bounded_uint<0u, 255u> a {u8{5}}; + BOOST_TEST_THROWS((static_cast>(a)), std::domain_error); + + // [0, 255] -> [10, 100]: value 0 < 10 + constexpr bounded_uint<0u, 255u> b {u8{0}}; + BOOST_TEST_THROWS((static_cast>(b)), std::domain_error); + + // [0, 255] -> [10, 100]: value 101 > 100, above target max + constexpr bounded_uint<0u, 255u> c {u8{101}}; + BOOST_TEST_THROWS((static_cast>(c)), std::domain_error); + + // [0, 255] -> [10, 100]: value 255 > 100 + constexpr bounded_uint<0u, 255u> d {u8{255}}; + BOOST_TEST_THROWS((static_cast>(d)), std::domain_error); + + // [10, 200] -> [50, 150]: value 10 < 50 + constexpr bounded_uint<10u, 200u> e {u8{10}}; + BOOST_TEST_THROWS((static_cast>(e)), std::domain_error); + + // [10, 200] -> [50, 150]: value 200 > 150 + constexpr bounded_uint<10u, 200u> f {u8{200}}; + BOOST_TEST_THROWS((static_cast>(f)), std::domain_error); +} + +// ----------------------------------------------- +// Same basis type, u16 bounds +// ----------------------------------------------- + +void test_u16_to_u16_conversions() +{ + // [0, 65535] -> [256, 1000]: value 500 + constexpr bounded_uint<0u, 65535u> a {u16{500}}; + const auto b {static_cast>(a)}; + const bounded_uint<256u, 1000u> expected {u16{500}}; + BOOST_TEST(b == expected); + + // [256, 1000] -> [0, 65535]: value 500 (wider target always succeeds for in-range values) + constexpr bounded_uint<256u, 1000u> c {u16{500}}; + const auto d {static_cast>(c)}; + const bounded_uint<0u, 65535u> expected_d {u16{500}}; + BOOST_TEST(d == expected_d); + + // Throwing: [0, 65535] -> [256, 1000]: value 100 < 256 + constexpr bounded_uint<0u, 65535u> e {u16{100}}; + BOOST_TEST_THROWS((static_cast>(e)), std::domain_error); + + // Throwing: [0, 65535] -> [256, 1000]: value 2000 > 1000 + constexpr bounded_uint<0u, 65535u> f {u16{2000}}; + BOOST_TEST_THROWS((static_cast>(f)), std::domain_error); +} + +// ----------------------------------------------- +// Cross-width conversions: smaller to larger basis +// ----------------------------------------------- + +void test_u8_to_u16() +{ + // [0, 255] -> [0, 1000]: value 200 fits (u8 -> u16 widening) + constexpr bounded_uint<0u, 255u> a {u8{200}}; + const auto b {static_cast>(a)}; + const bounded_uint<0u, 1000u> expected {u16{200}}; + BOOST_TEST(b == expected); + + // [0, 255] -> [0, 65535]: full range value + constexpr bounded_uint<0u, 255u> c {u8{255}}; + const auto d {static_cast>(c)}; + const bounded_uint<0u, 65535u> expected_d {u16{255}}; + BOOST_TEST(d == expected_d); + + // [10, 100] -> [0, 40000]: value 50 + constexpr bounded_uint<10u, 100u> e {u8{50}}; + const auto f {static_cast>(e)}; + const bounded_uint<0u, 40000u> expected_f {u16{50}}; + BOOST_TEST(f == expected_f); +} + +void test_u8_to_u16_throwing() +{ + // [0, 255] -> [256, 1000]: any u8 value < 256, always below target min + constexpr bounded_uint<0u, 255u> a {u8{255}}; + BOOST_TEST_THROWS((static_cast>(a)), std::domain_error); + + constexpr bounded_uint<0u, 255u> b {u8{0}}; + BOOST_TEST_THROWS((static_cast>(b)), std::domain_error); +} + +void test_u8_to_u32() +{ + // [0, 255] -> [0, 100000]: value 100 + constexpr bounded_uint<0u, 255u> a {u8{100}}; + const auto b {static_cast>(a)}; + const bounded_uint<0u, 100000u> expected {u32{100}}; + BOOST_TEST(b == expected); +} + +void test_u16_to_u32() +{ + // [0, 1000] -> [0, 100000]: value 1000 + constexpr bounded_uint<0u, 1000u> a {u16{1000}}; + const auto b {static_cast>(a)}; + const bounded_uint<0u, 100000u> expected {u32{1000}}; + BOOST_TEST(b == expected); + + // [256, 65535] -> [0, 4294967295]: value 30000 + constexpr bounded_uint<256u, 65535u> c {u16{30000}}; + const auto d {static_cast>(c)}; + const bounded_uint<0u, 4294967295u> expected_d {u32{30000}}; + BOOST_TEST(d == expected_d); +} + +void test_u32_to_u64() +{ + // [0, 100000] -> [0, 5'000'000'000]: value 100000 + constexpr bounded_uint<0u, 100000u> a {u32{100000}}; + const auto b {static_cast>(a)}; + const bounded_uint<0ULL, 5'000'000'000ULL> expected {u64{100000}}; + BOOST_TEST(b == expected); + + // Throwing: [0, 100000] -> [4294967296, UINT64_MAX]: any u32 value < 4294967296 + constexpr bounded_uint<0u, 100000u> c {u32{100000}}; + BOOST_TEST_THROWS((static_cast>(c)), std::domain_error); +} + +void test_u64_to_u128() +{ + // [0, UINT64_MAX] -> [0, 2^64]: value fits + constexpr auto max_128 = uint128_t{1, 0}; + using b128 = bounded_uint; + + constexpr bounded_uint<0ULL, UINT64_MAX> a {u64{1'000'000ULL}}; + const auto b {static_cast(a)}; + const b128 expected {u128{uint128_t{1'000'000}}}; + BOOST_TEST(b == expected); + + // Max u64 value -> u128 range + constexpr bounded_uint<0ULL, UINT64_MAX> c {u64{UINT64_MAX}}; + const auto d {static_cast(c)}; + const b128 expected_d {u128{uint128_t{UINT64_MAX}}}; + BOOST_TEST(d == expected_d); +} + +// ----------------------------------------------- +// Overlapping ranges (partially compatible) +// ----------------------------------------------- + +void test_overlapping_ranges() +{ + // [10, 100] -> [50, 200]: value 75 is in both ranges + constexpr bounded_uint<10u, 100u> a {u8{75}}; + const auto b {static_cast>(a)}; + const bounded_uint<50u, 200u> expected {u8{75}}; + BOOST_TEST(b == expected); + + // [10, 100] -> [50, 200]: value 50 is exactly at target min + constexpr bounded_uint<10u, 100u> c {u8{50}}; + const auto d {static_cast>(c)}; + const bounded_uint<50u, 200u> expected_d {u8{50}}; + BOOST_TEST(d == expected_d); + + // [10, 100] -> [50, 200]: value 100 is exactly at source max, within target + constexpr bounded_uint<10u, 100u> e {u8{100}}; + const auto f {static_cast>(e)}; + const bounded_uint<50u, 200u> expected_f {u8{100}}; + BOOST_TEST(f == expected_f); + + // Throwing: [10, 100] -> [50, 200]: value 30 is in source but below target min + constexpr bounded_uint<10u, 100u> g {u8{30}}; + BOOST_TEST_THROWS((static_cast>(g)), std::domain_error); + + // [50, 200] -> [10, 100]: value 75 is in both ranges + constexpr bounded_uint<50u, 200u> h {u8{75}}; + const auto i {static_cast>(h)}; + const bounded_uint<10u, 100u> expected_i {u8{75}}; + BOOST_TEST(i == expected_i); + + // Throwing: [50, 200] -> [10, 100]: value 150 is in source but above target max + constexpr bounded_uint<50u, 200u> j {u8{150}}; + BOOST_TEST_THROWS((static_cast>(j)), std::domain_error); +} + +// ----------------------------------------------- +// Disjoint ranges (no overlap) +// ----------------------------------------------- + +void test_disjoint_ranges() +{ + // [0, 50] -> [100, 200]: any value from source < 100, always throws + constexpr bounded_uint<0u, 50u> a {u8{50}}; + BOOST_TEST_THROWS((static_cast>(a)), std::domain_error); + + constexpr bounded_uint<0u, 50u> b {u8{0}}; + BOOST_TEST_THROWS((static_cast>(b)), std::domain_error); + + // [100, 200] -> [0, 50]: any value from source > 50, always throws + constexpr bounded_uint<100u, 200u> c {u8{100}}; + BOOST_TEST_THROWS((static_cast>(c)), std::domain_error); + + constexpr bounded_uint<100u, 200u> d {u8{200}}; + BOOST_TEST_THROWS((static_cast>(d)), std::domain_error); + + // u16 disjoint: [0, 500] -> [1000, 2000] + constexpr bounded_uint<0u, 500u> e {u16{500}}; + BOOST_TEST_THROWS((static_cast>(e)), std::domain_error); +} + +// ----------------------------------------------- +// Identity conversion (same bounds) +// ----------------------------------------------- + +void test_identity_conversion() +{ + // [0, 255] -> [0, 255]: should always work + constexpr bounded_uint<0u, 255u> a {u8{42}}; + const auto b {static_cast>(a)}; + BOOST_TEST(a == b); + + constexpr bounded_uint<0u, 255u> c {u8{0}}; + const auto d {static_cast>(c)}; + BOOST_TEST(c == d); + + constexpr bounded_uint<0u, 255u> e {u8{255}}; + const auto f {static_cast>(e)}; + BOOST_TEST(e == f); + + // [10, 200] -> [10, 200] + constexpr bounded_uint<10u, 200u> g {u8{100}}; + const auto h {static_cast>(g)}; + BOOST_TEST(g == h); +} + +// ----------------------------------------------- +// Constexpr conversion tests +// ----------------------------------------------- + +void test_constexpr_conversions() +{ + // u8 -> u8 wider + constexpr bounded_uint<10u, 100u> a {u8{50}}; + constexpr auto b {static_cast>(a)}; + constexpr bounded_uint<0u, 255u> expected_b {u8{50}}; + static_assert(b == expected_b); + + // u8 -> u8 narrower + constexpr bounded_uint<0u, 255u> c {u8{75}}; + constexpr auto d {static_cast>(c)}; + constexpr bounded_uint<10u, 100u> expected_d {u8{75}}; + static_assert(d == expected_d); + + // u8 -> u16 widening + constexpr bounded_uint<0u, 255u> e {u8{200}}; + constexpr auto f {static_cast>(e)}; + constexpr bounded_uint<0u, 1000u> expected_f {u16{200}}; + static_assert(f == expected_f); + + // u16 -> u32 widening + constexpr bounded_uint<0u, 1000u> g {u16{1000}}; + constexpr auto h {static_cast>(g)}; + constexpr bounded_uint<0u, 100000u> expected_h {u32{1000}}; + static_assert(h == expected_h); + + // Identity + constexpr bounded_uint<0u, 255u> i {u8{42}}; + constexpr auto j {static_cast>(i)}; + static_assert(i == j); +} + +// ----------------------------------------------- +// u128 conversions +// ----------------------------------------------- + +void test_u128_conversions() +{ + constexpr auto max_128 = uint128_t{1, 0}; + constexpr auto u128_max = uint128_t{UINT64_MAX, UINT64_MAX}; + + // Narrower u128 range -> wider u128 range + using b128_narrow = bounded_uint; + using b128_wide = bounded_uint; + + const b128_narrow a {u128{uint128_t{5000}}}; + const auto b {static_cast(a)}; + const b128_wide expected {u128{uint128_t{5000}}}; + BOOST_TEST(b == expected); + + // Wider -> narrower, value in range + const b128_wide c {u128{uint128_t{5000}}}; + const auto d {static_cast(c)}; + const b128_narrow expected_d {u128{uint128_t{5000}}}; + BOOST_TEST(d == expected_d); + + // Wider -> narrower, value below min: 500 < 1000 + const b128_wide e {u128{uint128_t{500}}}; + BOOST_TEST_THROWS((static_cast(e)), std::domain_error); + + // Wider -> narrower, value above max: max_128 + 1 > max_128 + const b128_wide f {u128{uint128_t{1, 1}}}; + BOOST_TEST_THROWS((static_cast(f)), std::domain_error); +} + +// ----------------------------------------------- +// Narrowing conversions via operator OtherBasis() +// (bounded_uint -> smaller safe type) +// ----------------------------------------------- + +void test_bounded_to_basis_widening() +{ + // bounded_uint with u8 basis -> u16 (widening, always works) + constexpr bounded_uint<0u, 255u> a {u8{200}}; + const auto b {static_cast(a)}; + const u16 expected {200}; + BOOST_TEST(b == expected); + + // bounded_uint with u8 basis -> u32 + const auto c {static_cast(a)}; + const u32 expected_c {200}; + BOOST_TEST(c == expected_c); + + // bounded_uint with u16 basis -> u32 + constexpr bounded_uint<0u, 1000u> d {u16{1000}}; + const auto e {static_cast(d)}; + const u32 expected_e {1000}; + BOOST_TEST(e == expected_e); + + // bounded_uint with u16 basis -> u64 + const auto f {static_cast(d)}; + const u64 expected_f {1000}; + BOOST_TEST(f == expected_f); + + // bounded_uint with u32 basis -> u64 + constexpr bounded_uint<0u, 100000u> g {u32{100000}}; + const auto h {static_cast(g)}; + const u64 expected_h {100000}; + BOOST_TEST(h == expected_h); +} + +void test_bounded_to_basis_same_width() +{ + // bounded_uint with u8 basis -> u8 (same width, always works) + constexpr bounded_uint<0u, 255u> a {u8{42}}; + const auto b {static_cast(a)}; + const u8 expected {42}; + BOOST_TEST(b == expected); + + constexpr bounded_uint<10u, 100u> c {u8{75}}; + const auto d {static_cast(c)}; + const u8 expected_d {75}; + BOOST_TEST(d == expected_d); + + // bounded_uint with u16 basis -> u16 + constexpr bounded_uint<0u, 1000u> e {u16{500}}; + const auto f {static_cast(e)}; + const u16 expected_f {500}; + BOOST_TEST(f == expected_f); +} + +void test_bounded_to_basis_narrowing_fits() +{ + // bounded_uint with u16 basis -> u8: value 200 fits in u8 + constexpr bounded_uint<0u, 1000u> a {u16{200}}; + const auto b {static_cast(a)}; + const u8 expected {200}; + BOOST_TEST(b == expected); + + // bounded_uint with u16 basis -> u8: value 0 fits + constexpr bounded_uint<0u, 1000u> c {u16{0}}; + const auto d {static_cast(c)}; + const u8 expected_d {0}; + BOOST_TEST(d == expected_d); + + // bounded_uint with u16 basis -> u8: value 255 fits (exact max) + constexpr bounded_uint<0u, 1000u> e {u16{255}}; + const auto f {static_cast(e)}; + const u8 expected_f {255}; + BOOST_TEST(f == expected_f); + + // bounded_uint with u32 basis -> u16: value 30000 fits in u16 + constexpr bounded_uint<0u, 100000u> g {u32{30000}}; + const auto h {static_cast(g)}; + const u16 expected_h {30000}; + BOOST_TEST(h == expected_h); + + // bounded_uint with u32 basis -> u8: value 100 fits in u8 + constexpr bounded_uint<0u, 100000u> i {u32{100}}; + const auto j {static_cast(i)}; + const u8 expected_j {100}; + BOOST_TEST(j == expected_j); + + // bounded_uint with u64 basis -> u32: value fits + constexpr bounded_uint<0ULL, 5'000'000'000ULL> k {u64{1'000'000ULL}}; + const auto l {static_cast(k)}; + const u32 expected_l {1'000'000u}; + BOOST_TEST(l == expected_l); +} + +void test_bounded_to_basis_narrowing_throws() +{ + // bounded_uint with u16 basis -> u8: value 256 > 255 + constexpr bounded_uint<0u, 1000u> a {u16{256}}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + // bounded_uint with u16 basis -> u8: value 1000 > 255 + constexpr bounded_uint<0u, 1000u> b {u16{1000}}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); + + // bounded_uint with u32 basis -> u8: value 300 > 255 + constexpr bounded_uint<0u, 100000u> c {u32{300}}; + BOOST_TEST_THROWS((static_cast(c)), std::domain_error); + + // bounded_uint with u32 basis -> u16: value 70000 > 65535 + constexpr bounded_uint<0u, 100000u> d {u32{70000}}; + BOOST_TEST_THROWS((static_cast(d)), std::domain_error); + + // bounded_uint with u64 basis -> u32: value 5'000'000'000 > UINT32_MAX + constexpr bounded_uint<0ULL, 5'000'000'000ULL> e {u64{5'000'000'000ULL}}; + BOOST_TEST_THROWS((static_cast(e)), std::domain_error); + + // bounded_uint with u64 basis -> u8: value 1000 > 255 + constexpr bounded_uint<0ULL, 5'000'000'000ULL> f {u64{1000}}; + BOOST_TEST_THROWS((static_cast(f)), std::domain_error); + + // bounded_uint with u64 basis -> u16: value 100000 > 65535 + constexpr bounded_uint<0ULL, 5'000'000'000ULL> g {u64{100000}}; + BOOST_TEST_THROWS((static_cast(g)), std::domain_error); +} + +int main() +{ + test_u8_to_u8_wider(); + test_u8_to_u8_narrower(); + test_u8_to_u8_throwing(); + + test_u16_to_u16_conversions(); + + test_u8_to_u16(); + test_u8_to_u16_throwing(); + test_u8_to_u32(); + test_u16_to_u32(); + test_u32_to_u64(); + test_u64_to_u128(); + + test_overlapping_ranges(); + test_disjoint_ranges(); + test_identity_conversion(); + test_constexpr_conversions(); + test_u128_conversions(); + + // Narrowing operator OtherBasis() tests + test_bounded_to_basis_widening(); + test_bounded_to_basis_same_width(); + test_bounded_to_basis_narrowing_fits(); + test_bounded_to_basis_narrowing_throws(); + + return boost::report_errors(); +} diff --git a/test/test_unsigned_bounded_division.cpp b/test/test_unsigned_bounded_division.cpp new file mode 100644 index 0000000..5442886 --- /dev/null +++ b/test/test_unsigned_bounded_division.cpp @@ -0,0 +1,320 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +// 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 + +#include + +#include +#include +#include + +using namespace boost::safe_numbers; +using boost::int128::uint128_t; + +// ----------------------------------------------- +// u8 basis: bounded_uint with Max <= 255 +// ----------------------------------------------- + +void test_u8_valid_division() +{ + // [0, 255] full range + constexpr bounded_uint<0u, 255u> a {u8{100}}; + constexpr bounded_uint<0u, 255u> b {u8{10}}; + const auto c {a / b}; + const bounded_uint<0u, 255u> expected_c {u8{10}}; + BOOST_TEST(c == expected_c); + + // Divide by one (identity) + constexpr bounded_uint<0u, 255u> one {u8{1}}; + const auto d {a / one}; + BOOST_TEST(d == a); + + // Self-division = 1 + const auto e {a / a}; + const bounded_uint<0u, 255u> expected_e {u8{1}}; + BOOST_TEST(e == expected_e); + + // Integer division truncation: 255 / 2 = 127 + constexpr bounded_uint<0u, 255u> f {u8{255}}; + constexpr bounded_uint<0u, 255u> g {u8{2}}; + const auto h {f / g}; + const bounded_uint<0u, 255u> expected_h {u8{127}}; + BOOST_TEST(h == expected_h); + + // Narrower range [10, 200]: 200 / 10 = 20 + constexpr bounded_uint<10u, 200u> i {u8{200}}; + constexpr bounded_uint<10u, 200u> j {u8{10}}; + const auto k {i / j}; + const bounded_uint<10u, 200u> expected_k {u8{20}}; + BOOST_TEST(k == expected_k); +} + +void test_u8_throwing_division() +{ + // Division by zero throws domain_error from underlying u8 + constexpr bounded_uint<0u, 255u> a {u8{100}}; + constexpr bounded_uint<0u, 255u> zero {u8{0}}; + BOOST_TEST_THROWS(a / zero, std::domain_error); + + // Result below bounded min: + // [50, 200]: 60 / 50 = 1 < 50 + constexpr bounded_uint<50u, 200u> b {u8{60}}; + constexpr bounded_uint<50u, 200u> c {u8{50}}; + BOOST_TEST_THROWS(b / c, std::domain_error); + + // [10, 100]: 10 / 10 = 1 < 10 + constexpr bounded_uint<10u, 100u> d {u8{10}}; + constexpr bounded_uint<10u, 100u> e {u8{10}}; + BOOST_TEST_THROWS(d / e, std::domain_error); +} + +// ----------------------------------------------- +// u16 basis: bounded_uint with Max in (255, 65535] +// ----------------------------------------------- + +void test_u16_valid_division() +{ + // [0, 1000] + constexpr bounded_uint<0u, 1000u> a {u16{1000}}; + constexpr bounded_uint<0u, 1000u> b {u16{10}}; + const auto c {a / b}; + const bounded_uint<0u, 1000u> expected_c {u16{100}}; + BOOST_TEST(c == expected_c); + + // Divide by one + constexpr bounded_uint<0u, 1000u> one {u16{1}}; + const auto d {a / one}; + BOOST_TEST(d == a); + + // [256, 65535]: 65000 / 256 = 253... < 256, would throw. Use values that stay in range. + // 60000 / 1 = 60000 (divide by one already tested). + // 40000 / 2 = 20000... < 256? No, 20000 > 256. But wait, both operands must be in [256, 65535]. + // So minimum divisor is 256. 65535 / 1 won't work since 1 < 256. Let's test valid cases: + // 60000 / 300 = 200... < 256, throws. Hard to get valid division with non-zero min. + // Skip non-zero min for u16 valid division - covered in throwing tests. + constexpr bounded_uint<0u, 40000u> e {u16{40000}}; + constexpr bounded_uint<0u, 40000u> f {u16{8}}; + const auto g {e / f}; + const bounded_uint<0u, 40000u> expected_g {u16{5000}}; + BOOST_TEST(g == expected_g); +} + +void test_u16_throwing_division() +{ + // Division by zero + constexpr bounded_uint<0u, 1000u> a {u16{500}}; + constexpr bounded_uint<0u, 1000u> zero {u16{0}}; + BOOST_TEST_THROWS(a / zero, std::domain_error); + + // Result below bounded min: + // [256, 65535]: 65535 / 256 = 255 < 256 + constexpr bounded_uint<256u, 65535u> b {u16{65535}}; + constexpr bounded_uint<256u, 65535u> c {u16{256}}; + BOOST_TEST_THROWS(b / c, std::domain_error); + + // [500, 1000]: 600 / 500 = 1 < 500 + constexpr bounded_uint<500u, 1000u> d {u16{600}}; + constexpr bounded_uint<500u, 1000u> e {u16{500}}; + BOOST_TEST_THROWS(d / e, std::domain_error); +} + +// ----------------------------------------------- +// u32 basis: bounded_uint with Max in (65535, 4294967295] +// ----------------------------------------------- + +void test_u32_valid_division() +{ + // [0, 100000] + constexpr bounded_uint<0u, 100000u> a {u32{100000}}; + constexpr bounded_uint<0u, 100000u> b {u32{10}}; + const auto c {a / b}; + const bounded_uint<0u, 100000u> expected_c {u32{10000}}; + BOOST_TEST(c == expected_c); + + // Divide by one + constexpr bounded_uint<0u, 100000u> one {u32{1}}; + const auto d {a / one}; + BOOST_TEST(d == a); + + // [65536, 4294967295]: self division = 1... < 65536. Non-zero min makes valid division very constrained. + // 4000000000 / 65536 = 61035 < 65536 -> throws. Use a range that allows valid results. + // [1, 4294967295]: 4000000000 / 2 = 2000000000 + constexpr bounded_uint<1u, 4294967295u> e {u32{4000000000u}}; + constexpr bounded_uint<1u, 4294967295u> f {u32{2}}; + const auto g {e / f}; + const bounded_uint<1u, 4294967295u> expected_g {u32{2000000000u}}; + BOOST_TEST(g == expected_g); +} + +void test_u32_throwing_division() +{ + // Division by zero + constexpr bounded_uint<0u, 100000u> a {u32{50000}}; + constexpr bounded_uint<0u, 100000u> zero {u32{0}}; + BOOST_TEST_THROWS(a / zero, std::domain_error); + + // Result below bounded min: + // [65536, 4294967295]: 1000000 / 65536 = 15 < 65536 + constexpr bounded_uint<65536u, 4294967295u> b {u32{1000000}}; + constexpr bounded_uint<65536u, 4294967295u> c {u32{65536}}; + BOOST_TEST_THROWS(b / c, std::domain_error); +} + +// ----------------------------------------------- +// u64 basis: bounded_uint with Max in (4294967295, UINT64_MAX] +// ----------------------------------------------- + +void test_u64_valid_division() +{ + // [0, 5'000'000'000] + constexpr bounded_uint<0ULL, 5'000'000'000ULL> a {u64{5'000'000'000ULL}}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> b {u64{5ULL}}; + const auto c {a / b}; + const bounded_uint<0ULL, 5'000'000'000ULL> expected_c {u64{1'000'000'000ULL}}; + BOOST_TEST(c == expected_c); + + // Divide by one + constexpr bounded_uint<0ULL, 5'000'000'000ULL> one {u64{1}}; + const auto d {a / one}; + BOOST_TEST(d == a); + + // Self division + const auto e {a / a}; + const bounded_uint<0ULL, 5'000'000'000ULL> expected_e {u64{1}}; + BOOST_TEST(e == expected_e); +} + +void test_u64_throwing_division() +{ + // Division by zero + constexpr bounded_uint<0ULL, UINT64_MAX> a {u64{1'000'000ULL}}; + constexpr bounded_uint<0ULL, UINT64_MAX> zero {u64{0}}; + BOOST_TEST_THROWS(a / zero, std::domain_error); + + // Result below bounded min: + // [4294967296, UINT64_MAX]: any_val / any_val = 1 < 4294967296 + constexpr bounded_uint<4294967296ULL, UINT64_MAX> b {u64{10'000'000'000ULL}}; + constexpr bounded_uint<4294967296ULL, UINT64_MAX> c {u64{10'000'000'000ULL}}; + BOOST_TEST_THROWS(b / c, std::domain_error); +} + +// ----------------------------------------------- +// u128 basis: bounded_uint with Max > UINT64_MAX +// ----------------------------------------------- + +void test_u128_valid_division() +{ + constexpr auto max_val = uint128_t{1, 0}; + using b128 = bounded_uint; + + const b128 a {u128{uint128_t{10000}}}; + const b128 b {u128{uint128_t{100}}}; + const auto c {a / b}; + const b128 expected_c {u128{uint128_t{100}}}; + BOOST_TEST(c == expected_c); + + // Divide by one + const b128 one {u128{uint128_t{1}}}; + const auto d {a / one}; + BOOST_TEST(d == a); + + // Full u128 range + constexpr auto u128_max = uint128_t{UINT64_MAX, UINT64_MAX}; + using b128_full = bounded_uint; + + const b128_full e {u128{uint128_t{3'000'000}}}; + const b128_full f {u128{uint128_t{3}}}; + const auto g {e / f}; + const b128_full expected_g {u128{uint128_t{1'000'000}}}; + BOOST_TEST(g == expected_g); +} + +void test_u128_throwing_division() +{ + constexpr auto max_val = uint128_t{1, 0}; + using b128 = bounded_uint; + + // Division by zero + const b128 a {u128{uint128_t{100}}}; + const b128 zero {u128{uint128_t{0}}}; + BOOST_TEST_THROWS(a / zero, std::domain_error); + + // Non-zero min: [1000, 2^64]: 1500 / 1000 = 1 < 1000 + constexpr auto min_val = uint128_t{1000}; + using b128_ranged = bounded_uint; + + const b128_ranged b {u128{uint128_t{1500}}}; + const b128_ranged c {u128{uint128_t{1000}}}; + BOOST_TEST_THROWS(b / c, std::domain_error); +} + +// ----------------------------------------------- +// Constexpr division tests +// ----------------------------------------------- + +void test_constexpr_division() +{ + // u8 basis + constexpr bounded_uint<0u, 255u> a {u8{100}}; + constexpr bounded_uint<0u, 255u> b {u8{10}}; + constexpr auto c {a / b}; + constexpr bounded_uint<0u, 255u> expected_c {u8{10}}; + static_assert(c == expected_c); + + // u16 basis + constexpr bounded_uint<0u, 1000u> d {u16{1000}}; + constexpr bounded_uint<0u, 1000u> e {u16{10}}; + constexpr auto f {d / e}; + constexpr bounded_uint<0u, 1000u> expected_f {u16{100}}; + static_assert(f == expected_f); + + // u32 basis + constexpr bounded_uint<0u, 100000u> g {u32{100000}}; + constexpr bounded_uint<0u, 100000u> h {u32{10}}; + constexpr auto i {g / h}; + constexpr bounded_uint<0u, 100000u> expected_i {u32{10000}}; + static_assert(i == expected_i); + + // u64 basis + constexpr bounded_uint<0ULL, 5'000'000'000ULL> j {u64{5'000'000'000ULL}}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> k {u64{5ULL}}; + constexpr auto l {j / k}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> expected_l {u64{1'000'000'000ULL}}; + static_assert(l == expected_l); +} + +int main() +{ + test_u8_valid_division(); + test_u8_throwing_division(); + + test_u16_valid_division(); + test_u16_throwing_division(); + + test_u32_valid_division(); + test_u32_throwing_division(); + + test_u64_valid_division(); + test_u64_throwing_division(); + + test_u128_valid_division(); + test_u128_throwing_division(); + + test_constexpr_division(); + + return boost::report_errors(); +} diff --git a/test/test_unsigned_bounded_fmt_format.cpp b/test/test_unsigned_bounded_fmt_format.cpp new file mode 100644 index 0000000..23d07dc --- /dev/null +++ b/test/test_unsigned_bounded_fmt_format.cpp @@ -0,0 +1,95 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wfloat-equal" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wfloat-equal" +# pragma GCC diagnostic ignored "-Wsign-conversion" +# pragma GCC diagnostic ignored "-Wconversion" +#endif + +#define FMT_HEADER_ONLY + +#if __has_include() + +#include +#include +#include +#include + +using namespace boost::safe_numbers; + +template +void test() +{ + const T x {42}; + + BOOST_TEST_CSTR_EQ(fmt::format("{}", x).c_str(), "42"); + BOOST_TEST_CSTR_EQ(fmt::format("{:08x}", x).c_str(), "0000002a"); + BOOST_TEST_CSTR_EQ(fmt::format("{:#010b}", x).c_str(), "0b00101010"); +} + +void test_boundary_values() +{ + // u8-backed + { + using type = bounded_uint<0u, 255u>; + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{0}).c_str(), "0"); + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{255}).c_str(), "255"); + } + + // u16-backed + { + using type = bounded_uint<0u, 40000u>; + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{0}).c_str(), "0"); + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{40000}).c_str(), "40000"); + } + + // u32-backed + { + using type = bounded_uint<0u, 100000u>; + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{0}).c_str(), "0"); + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{100000}).c_str(), "100000"); + } + + // u64-backed + { + using type = bounded_uint<0ULL, 5'000'000'000ULL>; + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{0}).c_str(), "0"); + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{5'000'000'000ULL}).c_str(), "5000000000"); + } + + // Non-zero minimum + { + using type = bounded_uint<10u, 200u>; + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{10}).c_str(), "10"); + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{100}).c_str(), "100"); + BOOST_TEST_CSTR_EQ(fmt::format("{}", type{200}).c_str(), "200"); + BOOST_TEST_CSTR_EQ(fmt::format("{:04x}", type{200}).c_str(), "00c8"); + } +} + +int main() +{ + test>(); + test>(); + test>(); + test>(); + + test_boundary_values(); + + return boost::report_errors(); +} + +#else + +int main() +{ + return 0; +} + +#endif diff --git a/test/test_unsigned_bounded_modulo.cpp b/test/test_unsigned_bounded_modulo.cpp new file mode 100644 index 0000000..a1bce3d --- /dev/null +++ b/test/test_unsigned_bounded_modulo.cpp @@ -0,0 +1,317 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +// 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 + +#include + +#include +#include +#include + +using namespace boost::safe_numbers; +using boost::int128::uint128_t; + +// ----------------------------------------------- +// u8 basis: bounded_uint with Max <= 255 +// ----------------------------------------------- + +void test_u8_valid_modulo() +{ + // [0, 255] full range + constexpr bounded_uint<0u, 255u> a {u8{100}}; + constexpr bounded_uint<0u, 255u> b {u8{30}}; + const auto c {a % b}; + const bounded_uint<0u, 255u> expected_c {u8{10}}; // 100 % 30 = 10 + BOOST_TEST(c == expected_c); + + // Modulo by one = 0 + constexpr bounded_uint<0u, 255u> one {u8{1}}; + const auto d {a % one}; + const bounded_uint<0u, 255u> expected_d {u8{0}}; + BOOST_TEST(d == expected_d); + + // Self modulo = 0 + const auto e {a % a}; + const bounded_uint<0u, 255u> expected_e {u8{0}}; + BOOST_TEST(e == expected_e); + + // Modulo with remainder: 255 % 100 = 55 + constexpr bounded_uint<0u, 255u> f {u8{255}}; + constexpr bounded_uint<0u, 255u> g {u8{100}}; + const auto h {f % g}; + const bounded_uint<0u, 255u> expected_h {u8{55}}; + BOOST_TEST(h == expected_h); + + // Smaller dividend: 10 % 100 = 10 + constexpr bounded_uint<0u, 255u> i {u8{10}}; + const auto j {i % g}; + const bounded_uint<0u, 255u> expected_j {u8{10}}; + BOOST_TEST(j == expected_j); +} + +void test_u8_throwing_modulo() +{ + // Modulo by zero throws domain_error from underlying u8 + constexpr bounded_uint<0u, 255u> a {u8{100}}; + constexpr bounded_uint<0u, 255u> zero {u8{0}}; + BOOST_TEST_THROWS(a % zero, std::domain_error); + + // Result below bounded min: + // [50, 200]: 100 % 60 = 40 < 50 + constexpr bounded_uint<50u, 200u> b {u8{100}}; + constexpr bounded_uint<50u, 200u> c {u8{60}}; + BOOST_TEST_THROWS(b % c, std::domain_error); + + // [10, 100]: 15 % 10 = 5 < 10 + constexpr bounded_uint<10u, 100u> d {u8{15}}; + constexpr bounded_uint<10u, 100u> e {u8{10}}; + BOOST_TEST_THROWS(d % e, std::domain_error); +} + +// ----------------------------------------------- +// u16 basis: bounded_uint with Max in (255, 65535] +// ----------------------------------------------- + +void test_u16_valid_modulo() +{ + // [0, 1000] + constexpr bounded_uint<0u, 1000u> a {u16{1000}}; + constexpr bounded_uint<0u, 1000u> b {u16{300}}; + const auto c {a % b}; + const bounded_uint<0u, 1000u> expected_c {u16{100}}; // 1000 % 300 = 100 + BOOST_TEST(c == expected_c); + + // Modulo by one = 0 + constexpr bounded_uint<0u, 1000u> one {u16{1}}; + const auto d {a % one}; + const bounded_uint<0u, 1000u> expected_d {u16{0}}; + BOOST_TEST(d == expected_d); + + // [0, 40000] + constexpr bounded_uint<0u, 40000u> e {u16{35000}}; + constexpr bounded_uint<0u, 40000u> f {u16{10000}}; + const auto g {e % f}; + const bounded_uint<0u, 40000u> expected_g {u16{5000}}; // 35000 % 10000 = 5000 + BOOST_TEST(g == expected_g); +} + +void test_u16_throwing_modulo() +{ + // Modulo by zero + constexpr bounded_uint<0u, 1000u> a {u16{500}}; + constexpr bounded_uint<0u, 1000u> zero {u16{0}}; + BOOST_TEST_THROWS(a % zero, std::domain_error); + + // Result below bounded min: + // [256, 65535]: 300 % 256 = 44 < 256 + constexpr bounded_uint<256u, 65535u> b {u16{300}}; + constexpr bounded_uint<256u, 65535u> c {u16{256}}; + BOOST_TEST_THROWS(b % c, std::domain_error); + + // [500, 1000]: 700 % 500 = 200 < 500 + constexpr bounded_uint<500u, 1000u> d {u16{700}}; + constexpr bounded_uint<500u, 1000u> e {u16{500}}; + BOOST_TEST_THROWS(d % e, std::domain_error); +} + +// ----------------------------------------------- +// u32 basis: bounded_uint with Max in (65535, 4294967295] +// ----------------------------------------------- + +void test_u32_valid_modulo() +{ + // [0, 100000] + constexpr bounded_uint<0u, 100000u> a {u32{100000}}; + constexpr bounded_uint<0u, 100000u> b {u32{30000}}; + const auto c {a % b}; + const bounded_uint<0u, 100000u> expected_c {u32{10000}}; // 100000 % 30000 = 10000 + BOOST_TEST(c == expected_c); + + // Modulo by one = 0 + constexpr bounded_uint<0u, 100000u> one {u32{1}}; + const auto d {a % one}; + const bounded_uint<0u, 100000u> expected_d {u32{0}}; + BOOST_TEST(d == expected_d); + + // [0, 4294967295] + constexpr bounded_uint<0u, 4294967295u> e {u32{4294967295u}}; + constexpr bounded_uint<0u, 4294967295u> f {u32{1000000000u}}; + const auto g {e % f}; + const bounded_uint<0u, 4294967295u> expected_g {u32{4294967295u % 1000000000u}}; + BOOST_TEST(g == expected_g); +} + +void test_u32_throwing_modulo() +{ + // Modulo by zero + constexpr bounded_uint<0u, 100000u> a {u32{50000}}; + constexpr bounded_uint<0u, 100000u> zero {u32{0}}; + BOOST_TEST_THROWS(a % zero, std::domain_error); + + // Result below bounded min: + // [65536, 4294967295]: 100000 % 65536 = 34464 < 65536 + constexpr bounded_uint<65536u, 4294967295u> b {u32{100000}}; + constexpr bounded_uint<65536u, 4294967295u> c {u32{65536}}; + BOOST_TEST_THROWS(b % c, std::domain_error); +} + +// ----------------------------------------------- +// u64 basis: bounded_uint with Max in (4294967295, UINT64_MAX] +// ----------------------------------------------- + +void test_u64_valid_modulo() +{ + // [0, 5'000'000'000] + constexpr bounded_uint<0ULL, 5'000'000'000ULL> a {u64{5'000'000'000ULL}}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> b {u64{3'000'000'000ULL}}; + const auto c {a % b}; + const bounded_uint<0ULL, 5'000'000'000ULL> expected_c {u64{2'000'000'000ULL}}; + BOOST_TEST(c == expected_c); + + // Modulo by one = 0 + constexpr bounded_uint<0ULL, 5'000'000'000ULL> one {u64{1}}; + const auto d {a % one}; + const bounded_uint<0ULL, 5'000'000'000ULL> expected_d {u64{0}}; + BOOST_TEST(d == expected_d); + + // Self modulo = 0 + const auto e {a % a}; + const bounded_uint<0ULL, 5'000'000'000ULL> expected_e {u64{0}}; + BOOST_TEST(e == expected_e); +} + +void test_u64_throwing_modulo() +{ + // Modulo by zero + constexpr bounded_uint<0ULL, UINT64_MAX> a {u64{1'000'000ULL}}; + constexpr bounded_uint<0ULL, UINT64_MAX> zero {u64{0}}; + BOOST_TEST_THROWS(a % zero, std::domain_error); + + // Result below bounded min: + // [4294967296, UINT64_MAX]: 5'000'000'000 % 4294967296 = 705032704 < 4294967296 + constexpr bounded_uint<4294967296ULL, UINT64_MAX> b {u64{5'000'000'000ULL}}; + constexpr bounded_uint<4294967296ULL, UINT64_MAX> c {u64{4294967296ULL}}; + BOOST_TEST_THROWS(b % c, std::domain_error); +} + +// ----------------------------------------------- +// u128 basis: bounded_uint with Max > UINT64_MAX +// ----------------------------------------------- + +void test_u128_valid_modulo() +{ + constexpr auto max_val = uint128_t{1, 0}; + using b128 = bounded_uint; + + const b128 a {u128{uint128_t{10000}}}; + const b128 b {u128{uint128_t{3000}}}; + const auto c {a % b}; + const b128 expected_c {u128{uint128_t{1000}}}; // 10000 % 3000 = 1000 + BOOST_TEST(c == expected_c); + + // Modulo by one = 0 + const b128 one {u128{uint128_t{1}}}; + const auto d {a % one}; + const b128 expected_d {u128{uint128_t{0}}}; + BOOST_TEST(d == expected_d); + + // Full u128 range + constexpr auto u128_max = uint128_t{UINT64_MAX, UINT64_MAX}; + using b128_full = bounded_uint; + + const b128_full e {u128{uint128_t{3'000'000}}}; + const b128_full f {u128{uint128_t{1'000'000}}}; + const auto g {e % f}; + const b128_full expected_g {u128{uint128_t{0}}}; + BOOST_TEST(g == expected_g); +} + +void test_u128_throwing_modulo() +{ + constexpr auto max_val = uint128_t{1, 0}; + using b128 = bounded_uint; + + // Modulo by zero + const b128 a {u128{uint128_t{100}}}; + const b128 zero {u128{uint128_t{0}}}; + BOOST_TEST_THROWS(a % zero, std::domain_error); + + // Non-zero min: [1000, 2^64]: 1500 % 1000 = 500 < 1000 + constexpr auto min_val = uint128_t{1000}; + using b128_ranged = bounded_uint; + + const b128_ranged b {u128{uint128_t{1500}}}; + const b128_ranged c {u128{uint128_t{1000}}}; + BOOST_TEST_THROWS(b % c, std::domain_error); +} + +// ----------------------------------------------- +// Constexpr modulo tests +// ----------------------------------------------- + +void test_constexpr_modulo() +{ + // u8 basis + constexpr bounded_uint<0u, 255u> a {u8{100}}; + constexpr bounded_uint<0u, 255u> b {u8{30}}; + constexpr auto c {a % b}; + constexpr bounded_uint<0u, 255u> expected_c {u8{10}}; + static_assert(c == expected_c); + + // u16 basis + constexpr bounded_uint<0u, 1000u> d {u16{1000}}; + constexpr bounded_uint<0u, 1000u> e {u16{300}}; + constexpr auto f {d % e}; + constexpr bounded_uint<0u, 1000u> expected_f {u16{100}}; + static_assert(f == expected_f); + + // u32 basis + constexpr bounded_uint<0u, 100000u> g {u32{100000}}; + constexpr bounded_uint<0u, 100000u> h {u32{30000}}; + constexpr auto i {g % h}; + constexpr bounded_uint<0u, 100000u> expected_i {u32{10000}}; + static_assert(i == expected_i); + + // u64 basis + constexpr bounded_uint<0ULL, 5'000'000'000ULL> j {u64{5'000'000'000ULL}}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> k {u64{3'000'000'000ULL}}; + constexpr auto l {j % k}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> expected_l {u64{2'000'000'000ULL}}; + static_assert(l == expected_l); +} + +int main() +{ + test_u8_valid_modulo(); + test_u8_throwing_modulo(); + + test_u16_valid_modulo(); + test_u16_throwing_modulo(); + + test_u32_valid_modulo(); + test_u32_throwing_modulo(); + + test_u64_valid_modulo(); + test_u64_throwing_modulo(); + + test_u128_valid_modulo(); + test_u128_throwing_modulo(); + + test_constexpr_modulo(); + + return boost::report_errors(); +} diff --git a/test/test_unsigned_bounded_multiplication.cpp b/test/test_unsigned_bounded_multiplication.cpp new file mode 100644 index 0000000..61b3309 --- /dev/null +++ b/test/test_unsigned_bounded_multiplication.cpp @@ -0,0 +1,311 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +// 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 + +#include + +#include +#include +#include + +using namespace boost::safe_numbers; +using boost::int128::uint128_t; + +// ----------------------------------------------- +// u8 basis: bounded_uint with Max <= 255 +// ----------------------------------------------- + +void test_u8_valid_multiplication() +{ + // [0, 255] full range + constexpr bounded_uint<0u, 255u> a {u8{5}}; + constexpr bounded_uint<0u, 255u> b {u8{10}}; + const auto c {a * b}; + const bounded_uint<0u, 255u> expected_c {u8{50}}; + BOOST_TEST(c == expected_c); + + // Multiply by one (identity) + constexpr bounded_uint<0u, 255u> one {u8{1}}; + const auto d {a * one}; + BOOST_TEST(d == a); + + // Multiply by zero + constexpr bounded_uint<0u, 255u> zero {u8{0}}; + const auto e {a * zero}; + const bounded_uint<0u, 255u> expected_e {u8{0}}; + BOOST_TEST(e == expected_e); + + // Max boundary: 15 * 17 = 255 + constexpr bounded_uint<0u, 255u> f {u8{15}}; + constexpr bounded_uint<0u, 255u> g {u8{17}}; + const auto h {f * g}; + const bounded_uint<0u, 255u> expected_h {u8{255}}; + BOOST_TEST(h == expected_h); + + // Narrower range [10, 200]: 10 * 10 = 100 + constexpr bounded_uint<10u, 200u> i {u8{10}}; + constexpr bounded_uint<10u, 200u> j {u8{10}}; + const auto k {i * j}; + const bounded_uint<10u, 200u> expected_k {u8{100}}; + BOOST_TEST(k == expected_k); +} + +void test_u8_throwing_multiplication() +{ + // Overflow of underlying u8: 20 * 20 = 400 > 255 + constexpr bounded_uint<0u, 255u> a {u8{20}}; + constexpr bounded_uint<0u, 255u> b {u8{20}}; + BOOST_TEST_THROWS(a * b, std::overflow_error); + + // Result within u8 range but exceeds bounded max: + // [10, 100]: 11 * 10 = 110 > 100 + constexpr bounded_uint<10u, 100u> c {u8{11}}; + constexpr bounded_uint<10u, 100u> d {u8{10}}; + BOOST_TEST_THROWS(c * d, std::domain_error); +} + +// ----------------------------------------------- +// u16 basis: bounded_uint with Max in (255, 65535] +// ----------------------------------------------- + +void test_u16_valid_multiplication() +{ + // [0, 1000] + constexpr bounded_uint<0u, 1000u> a {u16{10}}; + constexpr bounded_uint<0u, 1000u> b {u16{50}}; + const auto c {a * b}; + const bounded_uint<0u, 1000u> expected_c {u16{500}}; + BOOST_TEST(c == expected_c); + + // Multiply by one + constexpr bounded_uint<0u, 1000u> one {u16{1}}; + const auto d {a * one}; + BOOST_TEST(d == a); + + // Max boundary: 100 * 10 = 1000 + constexpr bounded_uint<0u, 1000u> e {u16{100}}; + constexpr bounded_uint<0u, 1000u> f {u16{10}}; + const auto g {e * f}; + const bounded_uint<0u, 1000u> expected_g {u16{1000}}; + BOOST_TEST(g == expected_g); + + // [0, 40000]: 100 * 400 = 40000 + constexpr bounded_uint<0u, 40000u> h {u16{100}}; + constexpr bounded_uint<0u, 40000u> i {u16{400}}; + const auto j {h * i}; + const bounded_uint<0u, 40000u> expected_j {u16{40000}}; + BOOST_TEST(j == expected_j); +} + +void test_u16_throwing_multiplication() +{ + // Overflow of underlying u16: 300 * 300 = 90000 > 65535 + constexpr bounded_uint<0u, 65535u> a {u16{300}}; + constexpr bounded_uint<0u, 65535u> b {u16{300}}; + BOOST_TEST_THROWS(a * b, std::overflow_error); + + // Result within u16 range but exceeds bounded max: + // [0, 1000]: 100 * 20 = 2000 > 1000 + constexpr bounded_uint<0u, 1000u> c {u16{100}}; + constexpr bounded_uint<0u, 1000u> d {u16{20}}; + BOOST_TEST_THROWS(c * d, std::domain_error); +} + +// ----------------------------------------------- +// u32 basis: bounded_uint with Max in (65535, 4294967295] +// ----------------------------------------------- + +void test_u32_valid_multiplication() +{ + // [0, 100000] + constexpr bounded_uint<0u, 100000u> a {u32{100}}; + constexpr bounded_uint<0u, 100000u> b {u32{500}}; + const auto c {a * b}; + const bounded_uint<0u, 100000u> expected_c {u32{50000}}; + BOOST_TEST(c == expected_c); + + // Multiply by one + constexpr bounded_uint<0u, 100000u> one {u32{1}}; + const auto d {a * one}; + BOOST_TEST(d == a); + + // Max boundary: 1000 * 100 = 100000 + constexpr bounded_uint<0u, 100000u> e {u32{1000}}; + constexpr bounded_uint<0u, 100000u> f {u32{100}}; + const auto g {e * f}; + const bounded_uint<0u, 100000u> expected_g {u32{100000}}; + BOOST_TEST(g == expected_g); +} + +void test_u32_throwing_multiplication() +{ + // Overflow of underlying u32: 100000 * 100000 > UINT32_MAX + constexpr bounded_uint<0u, 4294967295u> a {u32{100000}}; + constexpr bounded_uint<0u, 4294967295u> b {u32{100000}}; + BOOST_TEST_THROWS(a * b, std::overflow_error); + + // Result within u32 range but exceeds bounded max: + // [0, 100000]: 500 * 500 = 250000 > 100000 + constexpr bounded_uint<0u, 100000u> c {u32{500}}; + constexpr bounded_uint<0u, 100000u> d {u32{500}}; + BOOST_TEST_THROWS(c * d, std::domain_error); +} + +// ----------------------------------------------- +// u64 basis: bounded_uint with Max in (4294967295, UINT64_MAX] +// ----------------------------------------------- + +void test_u64_valid_multiplication() +{ + // [0, 5'000'000'000] + constexpr bounded_uint<0ULL, 5'000'000'000ULL> a {u64{50'000ULL}}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> b {u64{100'000ULL}}; + const auto c {a * b}; + const bounded_uint<0ULL, 5'000'000'000ULL> expected_c {u64{5'000'000'000ULL}}; + BOOST_TEST(c == expected_c); + + // Multiply by one + constexpr bounded_uint<0ULL, 5'000'000'000ULL> one {u64{1}}; + const auto d {a * one}; + BOOST_TEST(d == a); + + // Self-multiplication with small value in wide range + constexpr bounded_uint<0ULL, UINT64_MAX> e {u64{1'000'000ULL}}; + constexpr bounded_uint<0ULL, UINT64_MAX> f {u64{1'000'000ULL}}; + const auto g {e * f}; + const bounded_uint<0ULL, UINT64_MAX> expected_g {u64{1'000'000'000'000ULL}}; + BOOST_TEST(g == expected_g); +} + +void test_u64_throwing_multiplication() +{ + // Overflow of underlying u64 + constexpr bounded_uint<0ULL, UINT64_MAX> a {u64{UINT64_MAX}}; + constexpr bounded_uint<0ULL, UINT64_MAX> b {u64{2}}; + BOOST_TEST_THROWS(a * b, std::overflow_error); + + // Result within u64 range but exceeds bounded max: + // [0, 5'000'000'000]: 100'000 * 100'000 = 10'000'000'000 > 5'000'000'000 + constexpr bounded_uint<0ULL, 5'000'000'000ULL> c {u64{100'000ULL}}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> d {u64{100'000ULL}}; + BOOST_TEST_THROWS(c * d, std::domain_error); +} + +// ----------------------------------------------- +// u128 basis: bounded_uint with Max > UINT64_MAX +// ----------------------------------------------- + +void test_u128_valid_multiplication() +{ + constexpr auto max_val = uint128_t{1, 0}; + using b128 = bounded_uint; + + const b128 a {u128{uint128_t{100}}}; + const b128 b {u128{uint128_t{200}}}; + const auto c {a * b}; + const b128 expected_c {u128{uint128_t{20000}}}; + BOOST_TEST(c == expected_c); + + // Multiply by one + const b128 one {u128{uint128_t{1}}}; + const auto d {a * one}; + BOOST_TEST(d == a); + + // Multiply by zero + const b128 zero {u128{uint128_t{0}}}; + const auto e {a * zero}; + const b128 expected_e {u128{uint128_t{0}}}; + BOOST_TEST(e == expected_e); + + // Full u128 range + constexpr auto u128_max = uint128_t{UINT64_MAX, UINT64_MAX}; + using b128_full = bounded_uint; + + const b128_full f {u128{uint128_t{1'000}}}; + const b128_full g {u128{uint128_t{2'000}}}; + const auto h {f * g}; + const b128_full expected_h {u128{uint128_t{2'000'000}}}; + BOOST_TEST(h == expected_h); +} + +void test_u128_throwing_multiplication() +{ + constexpr auto max_val = uint128_t{1, 0}; + using b128 = bounded_uint; + + // Result exceeds bounded max + const b128 a {u128{max_val}}; + const b128 b {u128{uint128_t{2}}}; + BOOST_TEST_THROWS(a * b, std::domain_error); +} + +// ----------------------------------------------- +// Constexpr multiplication tests +// ----------------------------------------------- + +void test_constexpr_multiplication() +{ + // u8 basis + constexpr bounded_uint<0u, 255u> a {u8{5}}; + constexpr bounded_uint<0u, 255u> b {u8{10}}; + constexpr auto c {a * b}; + constexpr bounded_uint<0u, 255u> expected_c {u8{50}}; + static_assert(c == expected_c); + + // u16 basis + constexpr bounded_uint<0u, 1000u> d {u16{10}}; + constexpr bounded_uint<0u, 1000u> e {u16{50}}; + constexpr auto f {d * e}; + constexpr bounded_uint<0u, 1000u> expected_f {u16{500}}; + static_assert(f == expected_f); + + // u32 basis + constexpr bounded_uint<0u, 100000u> g {u32{100}}; + constexpr bounded_uint<0u, 100000u> h {u32{500}}; + constexpr auto i {g * h}; + constexpr bounded_uint<0u, 100000u> expected_i {u32{50000}}; + static_assert(i == expected_i); + + // u64 basis + constexpr bounded_uint<0ULL, 5'000'000'000ULL> j {u64{50'000ULL}}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> k {u64{100'000ULL}}; + constexpr auto l {j * k}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> expected_l {u64{5'000'000'000ULL}}; + static_assert(l == expected_l); +} + +int main() +{ + test_u8_valid_multiplication(); + test_u8_throwing_multiplication(); + + test_u16_valid_multiplication(); + test_u16_throwing_multiplication(); + + test_u32_valid_multiplication(); + test_u32_throwing_multiplication(); + + test_u64_valid_multiplication(); + test_u64_throwing_multiplication(); + + test_u128_valid_multiplication(); + test_u128_throwing_multiplication(); + + test_constexpr_multiplication(); + + return boost::report_errors(); +} diff --git a/test/test_unsigned_bounded_std_format.cpp b/test/test_unsigned_bounded_std_format.cpp new file mode 100644 index 0000000..dac5877 --- /dev/null +++ b/test/test_unsigned_bounded_std_format.cpp @@ -0,0 +1,96 @@ +// Copyright 2026 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 + +#ifdef BOOST_SAFE_NUMBERS_DETAIL_INT128_HAS_FORMAT + +#include + +#endif + +#endif + +#ifdef BOOST_SAFE_NUMBERS_DETAIL_INT128_HAS_FORMAT + +#include + +using namespace boost::safe_numbers; + +template +void test() +{ + const T x {42}; + + BOOST_TEST_CSTR_EQ(std::format("{}", x).c_str(), "42"); + BOOST_TEST_CSTR_EQ(std::format("{:08x}", x).c_str(), "0000002a"); + BOOST_TEST_CSTR_EQ(std::format("{:#010b}", x).c_str(), "0b00101010"); +} + +void test_boundary_values() +{ + // u8-backed + { + using type = bounded_uint<0u, 255u>; + BOOST_TEST_CSTR_EQ(std::format("{}", type{0}).c_str(), "0"); + BOOST_TEST_CSTR_EQ(std::format("{}", type{255}).c_str(), "255"); + } + + // u16-backed + { + using type = bounded_uint<0u, 40000u>; + BOOST_TEST_CSTR_EQ(std::format("{}", type{0}).c_str(), "0"); + BOOST_TEST_CSTR_EQ(std::format("{}", type{40000}).c_str(), "40000"); + } + + // u32-backed + { + using type = bounded_uint<0u, 100000u>; + BOOST_TEST_CSTR_EQ(std::format("{}", type{0}).c_str(), "0"); + BOOST_TEST_CSTR_EQ(std::format("{}", type{100000}).c_str(), "100000"); + } + + // u64-backed + { + using type = bounded_uint<0ULL, 5'000'000'000ULL>; + BOOST_TEST_CSTR_EQ(std::format("{}", type{0}).c_str(), "0"); + BOOST_TEST_CSTR_EQ(std::format("{}", type{5'000'000'000ULL}).c_str(), "5000000000"); + } + + // Non-zero minimum + { + using type = bounded_uint<10u, 200u>; + BOOST_TEST_CSTR_EQ(std::format("{}", type{10}).c_str(), "10"); + BOOST_TEST_CSTR_EQ(std::format("{}", type{100}).c_str(), "100"); + BOOST_TEST_CSTR_EQ(std::format("{}", type{200}).c_str(), "200"); + BOOST_TEST_CSTR_EQ(std::format("{:04x}", type{200}).c_str(), "00c8"); + } +} + +int main() +{ + test>(); + test>(); + test>(); + test>(); + + test_boundary_values(); + + return boost::report_errors(); +} + +#else + +int main() +{ + return 0; +} + +#endif diff --git a/test/test_unsigned_bounded_subtraction.cpp b/test/test_unsigned_bounded_subtraction.cpp new file mode 100644 index 0000000..e2f062b --- /dev/null +++ b/test/test_unsigned_bounded_subtraction.cpp @@ -0,0 +1,328 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +// 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 + +#include + +#include +#include +#include + +using namespace boost::safe_numbers; +using boost::int128::uint128_t; + +// ----------------------------------------------- +// u8 basis: bounded_uint with Max <= 255 +// ----------------------------------------------- + +void test_u8_valid_subtraction() +{ + // [0, 255] full range + constexpr bounded_uint<0u, 255u> a {u8{30}}; + constexpr bounded_uint<0u, 255u> b {u8{10}}; + const auto c {a - b}; + const bounded_uint<0u, 255u> expected_c {u8{20}}; + BOOST_TEST(c == expected_c); + + // Subtracting zero + constexpr bounded_uint<0u, 255u> zero {u8{0}}; + const auto d {a - zero}; + BOOST_TEST(d == a); + + // Result is zero: 100 - 100 = 0 + constexpr bounded_uint<0u, 255u> e {u8{100}}; + constexpr bounded_uint<0u, 255u> f {u8{100}}; + const auto g {e - f}; + const bounded_uint<0u, 255u> expected_g {u8{0}}; + BOOST_TEST(g == expected_g); + + // Narrower range [10, 200]: 100 - 50 = 50 + constexpr bounded_uint<10u, 200u> h {u8{100}}; + constexpr bounded_uint<10u, 200u> i {u8{50}}; + const auto j {h - i}; + const bounded_uint<10u, 200u> expected_j {u8{50}}; + BOOST_TEST(j == expected_j); + + // Min boundary: 200 - 190 = 10 + constexpr bounded_uint<10u, 200u> k {u8{200}}; + constexpr bounded_uint<10u, 200u> l {u8{190}}; + const auto m {k - l}; + const bounded_uint<10u, 200u> expected_m {u8{10}}; + BOOST_TEST(m == expected_m); +} + +void test_u8_throwing_subtraction() +{ + // Underflow of underlying u8: 10 - 20 wraps unsigned, throws underflow_error + constexpr bounded_uint<0u, 255u> a {u8{10}}; + constexpr bounded_uint<0u, 255u> b {u8{20}}; + BOOST_TEST_THROWS(a - b, std::underflow_error); + + // Result within u8 range but below bounded min: + // [50, 200]: 60 - 55 = 5 < 50 + constexpr bounded_uint<50u, 200u> c {u8{60}}; + constexpr bounded_uint<50u, 200u> d {u8{55}}; + BOOST_TEST_THROWS(c - d, std::domain_error); + + // Just one below min: [10, 200]: 20 - 11 = 9 < 10 + constexpr bounded_uint<10u, 200u> e {u8{20}}; + constexpr bounded_uint<10u, 200u> f {u8{11}}; + BOOST_TEST_THROWS(e - f, std::domain_error); +} + +// ----------------------------------------------- +// u16 basis: bounded_uint with Max in (255, 65535] +// ----------------------------------------------- + +void test_u16_valid_subtraction() +{ + // [0, 1000] + constexpr bounded_uint<0u, 1000u> a {u16{500}}; + constexpr bounded_uint<0u, 1000u> b {u16{200}}; + const auto c {a - b}; + const bounded_uint<0u, 1000u> expected_c {u16{300}}; + BOOST_TEST(c == expected_c); + + // Subtracting zero + constexpr bounded_uint<0u, 1000u> zero {u16{0}}; + const auto d {a - zero}; + BOOST_TEST(d == a); + + // Result is zero + constexpr bounded_uint<0u, 1000u> e {u16{1000}}; + constexpr bounded_uint<0u, 1000u> f {u16{1000}}; + const auto g {e - f}; + const bounded_uint<0u, 1000u> expected_g {u16{0}}; + BOOST_TEST(g == expected_g); + + // [256, 65535] non-zero min + constexpr bounded_uint<256u, 65535u> h {u16{10000}}; + constexpr bounded_uint<256u, 65535u> i {u16{5000}}; + const auto j {h - i}; + const bounded_uint<256u, 65535u> expected_j {u16{5000}}; + BOOST_TEST(j == expected_j); +} + +void test_u16_throwing_subtraction() +{ + // Underflow of underlying u16: 100 - 200 wraps + constexpr bounded_uint<0u, 65535u> a {u16{100}}; + constexpr bounded_uint<0u, 65535u> b {u16{200}}; + BOOST_TEST_THROWS(a - b, std::underflow_error); + + // Result below bounded min: [500, 1000]: 600 - 550 = 50 < 500 + constexpr bounded_uint<500u, 1000u> c {u16{600}}; + constexpr bounded_uint<500u, 1000u> d {u16{550}}; + BOOST_TEST_THROWS(c - d, std::domain_error); + + // Just one below: [256, 65535]: 300 - 256 = 44... wait, need to go below 256 + // [256, 65535]: 300 - 256 = 44 -- but 44 < 256 + // Actually 300 - 45 = 255 < 256 + constexpr bounded_uint<256u, 65535u> e {u16{300}}; + constexpr bounded_uint<256u, 65535u> f {u16{256}}; + BOOST_TEST_THROWS(e - f, std::domain_error); +} + +// ----------------------------------------------- +// u32 basis: bounded_uint with Max in (65535, 4294967295] +// ----------------------------------------------- + +void test_u32_valid_subtraction() +{ + // [0, 100000] + constexpr bounded_uint<0u, 100000u> a {u32{80000}}; + constexpr bounded_uint<0u, 100000u> b {u32{30000}}; + const auto c {a - b}; + const bounded_uint<0u, 100000u> expected_c {u32{50000}}; + BOOST_TEST(c == expected_c); + + // Subtracting zero + constexpr bounded_uint<0u, 100000u> zero {u32{0}}; + const auto d {a - zero}; + BOOST_TEST(d == a); + + // [65536, 4294967295] non-zero min + constexpr bounded_uint<65536u, 4294967295u> e {u32{1000000}}; + constexpr bounded_uint<65536u, 4294967295u> f {u32{500000}}; + const auto g {e - f}; + const bounded_uint<65536u, 4294967295u> expected_g {u32{500000}}; + BOOST_TEST(g == expected_g); +} + +void test_u32_throwing_subtraction() +{ + // Underflow of underlying u32 + constexpr bounded_uint<0u, 4294967295u> a {u32{100}}; + constexpr bounded_uint<0u, 4294967295u> b {u32{200}}; + BOOST_TEST_THROWS(a - b, std::underflow_error); + + // Result below bounded min: [65536, 4294967295]: 100000 - 65536 = 34464 < 65536 + constexpr bounded_uint<65536u, 4294967295u> c {u32{100000}}; + constexpr bounded_uint<65536u, 4294967295u> d {u32{65536}}; + BOOST_TEST_THROWS(c - d, std::domain_error); + + // [0, 100000]: result valid in u32 but subtraction causes underflow + constexpr bounded_uint<0u, 100000u> e {u32{10}}; + constexpr bounded_uint<0u, 100000u> f {u32{20}}; + BOOST_TEST_THROWS(e - f, std::underflow_error); +} + +// ----------------------------------------------- +// u64 basis: bounded_uint with Max in (4294967295, UINT64_MAX] +// ----------------------------------------------- + +void test_u64_valid_subtraction() +{ + // [0, 5'000'000'000] + constexpr bounded_uint<0ULL, 5'000'000'000ULL> a {u64{4'000'000'000ULL}}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> b {u64{1'000'000'000ULL}}; + const auto c {a - b}; + const bounded_uint<0ULL, 5'000'000'000ULL> expected_c {u64{3'000'000'000ULL}}; + BOOST_TEST(c == expected_c); + + // Subtracting zero + constexpr bounded_uint<0ULL, 5'000'000'000ULL> zero {u64{0}}; + const auto d {a - zero}; + BOOST_TEST(d == a); + + // [4294967296, UINT64_MAX] non-zero min + constexpr bounded_uint<4294967296ULL, UINT64_MAX> e {u64{10'000'000'000ULL}}; + constexpr bounded_uint<4294967296ULL, UINT64_MAX> f {u64{5'000'000'000ULL}}; + const auto g {e - f}; + const bounded_uint<4294967296ULL, UINT64_MAX> expected_g {u64{5'000'000'000ULL}}; + BOOST_TEST(g == expected_g); +} + +void test_u64_throwing_subtraction() +{ + // Underflow of underlying u64 + constexpr bounded_uint<0ULL, UINT64_MAX> a {u64{0}}; + constexpr bounded_uint<0ULL, UINT64_MAX> b {u64{1}}; + BOOST_TEST_THROWS(a - b, std::underflow_error); + + // Result below bounded min: [4294967296, UINT64_MAX] + constexpr bounded_uint<4294967296ULL, UINT64_MAX> c {u64{5'000'000'000ULL}}; + constexpr bounded_uint<4294967296ULL, UINT64_MAX> d {u64{4294967296ULL}}; + BOOST_TEST_THROWS(c - d, std::domain_error); +} + +// ----------------------------------------------- +// u128 basis: bounded_uint with Max > UINT64_MAX +// ----------------------------------------------- + +void test_u128_valid_subtraction() +{ + constexpr auto max_val = uint128_t{1, 0}; + using b128 = bounded_uint; + + const b128 a {u128{uint128_t{300}}}; + const b128 b {u128{uint128_t{100}}}; + const auto c {a - b}; + const b128 expected_c {u128{uint128_t{200}}}; + BOOST_TEST(c == expected_c); + + // Subtracting zero + const b128 zero {u128{uint128_t{0}}}; + const auto d {a - zero}; + BOOST_TEST(d == a); + + // Full u128 range + constexpr auto u128_max = uint128_t{UINT64_MAX, UINT64_MAX}; + using b128_full = bounded_uint; + + const b128_full e {u128{uint128_t{3'000'000}}}; + const b128_full f {u128{uint128_t{1'000'000}}}; + const auto g {e - f}; + const b128_full expected_g {u128{uint128_t{2'000'000}}}; + BOOST_TEST(g == expected_g); +} + +void test_u128_throwing_subtraction() +{ + constexpr auto max_val = uint128_t{1, 0}; + using b128 = bounded_uint; + + // Underflow: 0 - 1 + const b128 a {u128{uint128_t{0}}}; + const b128 b {u128{uint128_t{1}}}; + BOOST_TEST_THROWS(a - b, std::underflow_error); + + // Non-zero min: [1000, 2^64]: 1500 - 1000 = 500 < 1000 + constexpr auto min_val = uint128_t{1000}; + using b128_ranged = bounded_uint; + + const b128_ranged c {u128{uint128_t{1500}}}; + const b128_ranged d {u128{uint128_t{1000}}}; + BOOST_TEST_THROWS(c - d, std::domain_error); +} + +// ----------------------------------------------- +// Constexpr subtraction tests +// ----------------------------------------------- + +void test_constexpr_subtraction() +{ + // u8 basis + constexpr bounded_uint<0u, 255u> a {u8{30}}; + constexpr bounded_uint<0u, 255u> b {u8{10}}; + constexpr auto c {a - b}; + constexpr bounded_uint<0u, 255u> expected_c {u8{20}}; + static_assert(c == expected_c); + + // u16 basis + constexpr bounded_uint<0u, 1000u> d {u16{500}}; + constexpr bounded_uint<0u, 1000u> e {u16{200}}; + constexpr auto f {d - e}; + constexpr bounded_uint<0u, 1000u> expected_f {u16{300}}; + static_assert(f == expected_f); + + // u32 basis + constexpr bounded_uint<0u, 100000u> g {u32{80000}}; + constexpr bounded_uint<0u, 100000u> h {u32{30000}}; + constexpr auto i {g - h}; + constexpr bounded_uint<0u, 100000u> expected_i {u32{50000}}; + static_assert(i == expected_i); + + // u64 basis + constexpr bounded_uint<0ULL, 5'000'000'000ULL> j {u64{4'000'000'000ULL}}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> k {u64{1'000'000'000ULL}}; + constexpr auto l {j - k}; + constexpr bounded_uint<0ULL, 5'000'000'000ULL> expected_l {u64{3'000'000'000ULL}}; + static_assert(l == expected_l); +} + +int main() +{ + test_u8_valid_subtraction(); + test_u8_throwing_subtraction(); + + test_u16_valid_subtraction(); + test_u16_throwing_subtraction(); + + test_u32_valid_subtraction(); + test_u32_throwing_subtraction(); + + test_u64_valid_subtraction(); + test_u64_throwing_subtraction(); + + test_u128_valid_subtraction(); + test_u128_throwing_subtraction(); + + test_constexpr_subtraction(); + + return boost::report_errors(); +} diff --git a/test/test_unsigned_narrowing_conversions.cpp b/test/test_unsigned_narrowing_conversions.cpp new file mode 100644 index 0000000..a1b6fdc --- /dev/null +++ b/test/test_unsigned_narrowing_conversions.cpp @@ -0,0 +1,282 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +// 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 + +#include + +#include +#include +#include + +using namespace boost::safe_numbers; + +// The conversion operator OtherBasis() converts to raw unsigned integral types +// (std::uint8_t, std::uint16_t, etc.), not to other library types. + +// ----------------------------------------------- +// Widening conversions (should always succeed) +// ----------------------------------------------- + +void test_widening_u8_to_larger() +{ + const u8 a {200}; + const auto b {static_cast(a)}; + BOOST_TEST_EQ(b, static_cast(200)); + + const auto c {static_cast(a)}; + BOOST_TEST_EQ(c, static_cast(200)); + + const auto d {static_cast(a)}; + BOOST_TEST_EQ(d, static_cast(200)); + + const u8 zero {0}; + BOOST_TEST_EQ(static_cast(zero), static_cast(0)); + BOOST_TEST_EQ(static_cast(zero), static_cast(0)); + BOOST_TEST_EQ(static_cast(zero), static_cast(0)); + + const u8 max8 {255}; + BOOST_TEST_EQ(static_cast(max8), static_cast(255)); + BOOST_TEST_EQ(static_cast(max8), static_cast(255)); + BOOST_TEST_EQ(static_cast(max8), static_cast(255)); +} + +void test_widening_u16_to_larger() +{ + const u16 a {65535}; + const auto b {static_cast(a)}; + BOOST_TEST_EQ(b, static_cast(65535)); + + const auto c {static_cast(a)}; + BOOST_TEST_EQ(c, static_cast(65535)); + + const u16 zero {0}; + BOOST_TEST_EQ(static_cast(zero), static_cast(0)); + BOOST_TEST_EQ(static_cast(zero), static_cast(0)); +} + +void test_widening_u32_to_u64() +{ + const u32 a {4294967295u}; + const auto b {static_cast(a)}; + BOOST_TEST_EQ(b, static_cast(4294967295u)); + + const u32 zero {0}; + BOOST_TEST_EQ(static_cast(zero), static_cast(0)); +} + +// ----------------------------------------------- +// Same-width conversions (identity, always succeed) +// ----------------------------------------------- + +void test_same_width() +{ + const u8 a {42}; + BOOST_TEST_EQ(static_cast(a), static_cast(42)); + + const u16 b {1000}; + BOOST_TEST_EQ(static_cast(b), static_cast(1000)); + + const u32 c {100000}; + BOOST_TEST_EQ(static_cast(c), static_cast(100000)); + + const u64 d {1'000'000'000ULL}; + BOOST_TEST_EQ(static_cast(d), static_cast(1'000'000'000ULL)); +} + +// ----------------------------------------------- +// Narrowing conversions that fit (should succeed) +// ----------------------------------------------- + +void test_narrowing_u16_to_u8_fits() +{ + const u16 a {0}; + BOOST_TEST_EQ(static_cast(a), static_cast(0)); + + const u16 b {255}; + BOOST_TEST_EQ(static_cast(b), static_cast(255)); + + const u16 c {42}; + BOOST_TEST_EQ(static_cast(c), static_cast(42)); +} + +void test_narrowing_u32_to_u8_fits() +{ + const u32 a {0}; + BOOST_TEST_EQ(static_cast(a), static_cast(0)); + + const u32 b {255}; + BOOST_TEST_EQ(static_cast(b), static_cast(255)); +} + +void test_narrowing_u32_to_u16_fits() +{ + const u32 a {0}; + BOOST_TEST_EQ(static_cast(a), static_cast(0)); + + const u32 b {65535}; + BOOST_TEST_EQ(static_cast(b), static_cast(65535)); +} + +void test_narrowing_u64_to_u8_fits() +{ + const u64 a {0}; + BOOST_TEST_EQ(static_cast(a), static_cast(0)); + + const u64 b {255}; + BOOST_TEST_EQ(static_cast(b), static_cast(255)); +} + +void test_narrowing_u64_to_u16_fits() +{ + const u64 a {65535}; + BOOST_TEST_EQ(static_cast(a), static_cast(65535)); +} + +void test_narrowing_u64_to_u32_fits() +{ + const u64 a {4294967295ULL}; + BOOST_TEST_EQ(static_cast(a), static_cast(4294967295u)); + + const u64 b {0}; + BOOST_TEST_EQ(static_cast(b), static_cast(0)); +} + +// ----------------------------------------------- +// Narrowing conversions that overflow (should throw) +// ----------------------------------------------- + +void test_narrowing_u16_to_u8_throws() +{ + // 256 > 255 (u8 max) + const u16 a {256}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + // Max u16 value + const u16 b {65535}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); + + // Just above u8 max + const u16 c {300}; + BOOST_TEST_THROWS((static_cast(c)), std::domain_error); +} + +void test_narrowing_u32_to_u8_throws() +{ + const u32 a {256}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + const u32 b {100000}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); +} + +void test_narrowing_u32_to_u16_throws() +{ + // 65536 > 65535 (u16 max) + const u32 a {65536}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + const u32 b {4294967295u}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); +} + +void test_narrowing_u64_to_u8_throws() +{ + const u64 a {256}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + const u64 b {1'000'000}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); +} + +void test_narrowing_u64_to_u16_throws() +{ + const u64 a {65536}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + const u64 b {UINT64_MAX}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); +} + +void test_narrowing_u64_to_u32_throws() +{ + // 4294967296 > UINT32_MAX + const u64 a {4294967296ULL}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + const u64 b {UINT64_MAX}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); +} + +// ----------------------------------------------- +// Boundary values (exact max fits, max+1 throws) +// ----------------------------------------------- + +void test_narrowing_boundaries() +{ + // u16 -> u8: 255 fits, 256 does not + const u16 fits {255}; + BOOST_TEST_EQ(static_cast(fits), static_cast(255)); + + const u16 no_fit {256}; + BOOST_TEST_THROWS((static_cast(no_fit)), std::domain_error); + + // u32 -> u16: 65535 fits, 65536 does not + const u32 fits32 {65535}; + BOOST_TEST_EQ(static_cast(fits32), static_cast(65535)); + + const u32 no_fit32 {65536}; + BOOST_TEST_THROWS((static_cast(no_fit32)), std::domain_error); + + // u64 -> u32: UINT32_MAX fits, UINT32_MAX+1 does not + const u64 fits64 {4294967295ULL}; + BOOST_TEST_EQ(static_cast(fits64), static_cast(4294967295u)); + + const u64 no_fit64 {4294967296ULL}; + BOOST_TEST_THROWS((static_cast(no_fit64)), std::domain_error); +} + +int main() +{ + // Widening (always succeed) + test_widening_u8_to_larger(); + test_widening_u16_to_larger(); + test_widening_u32_to_u64(); + + // Same-width + test_same_width(); + + // Narrowing that fits (succeed) + test_narrowing_u16_to_u8_fits(); + test_narrowing_u32_to_u8_fits(); + test_narrowing_u32_to_u16_fits(); + test_narrowing_u64_to_u8_fits(); + test_narrowing_u64_to_u16_fits(); + test_narrowing_u64_to_u32_fits(); + + // Narrowing that overflows (throw) + test_narrowing_u16_to_u8_throws(); + test_narrowing_u32_to_u8_throws(); + test_narrowing_u32_to_u16_throws(); + test_narrowing_u64_to_u8_throws(); + test_narrowing_u64_to_u16_throws(); + test_narrowing_u64_to_u32_throws(); + + // Boundary values + test_narrowing_boundaries(); + + return boost::report_errors(); +}