Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
** xref:examples.adoc#examples_mixed_sign[Mixed Signedness Arithmetic]
** xref:examples.adoc#examples_boost_math_random[Boost Math and Random Integration]
** xref:examples.adoc#examples_boost_charconv[Boost.Charconv Integration]
** xref:examples.adoc#examples_boost_container_hash[Boost.ContainerHash and Boost.Unordered Integration]
** xref:examples.adoc#examples_to_string[String Conversion (to_string)]
** xref:examples.adoc#examples_fmt_format[pass:[{fmt}] Integration]
** xref:examples.adoc#examples_cstdlib[`<cstdlib>` support (Combined div and mod)]
Expand Down
21 changes: 21 additions & 0 deletions doc/modules/ROOT/pages/api_reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,27 @@ Listed by analogous STL header.

| xref:utilities.adoc#checked[`ckd_mul`]
| Checked multiplication (C23 `<stdckdint.h>` contract)

| xref:utilities.adoc#comparison[`cmp_equal`]
| Signedness-safe equality (C++26 `<utility>` contract)

| xref:utilities.adoc#comparison[`cmp_not_equal`]
| Signedness-safe inequality

| xref:utilities.adoc#comparison[`cmp_less`]
| Signedness-safe less-than

| xref:utilities.adoc#comparison[`cmp_greater`]
| Signedness-safe greater-than

| xref:utilities.adoc#comparison[`cmp_less_equal`]
| Signedness-safe less-than-or-equal

| xref:utilities.adoc#comparison[`cmp_greater_equal`]
| Signedness-safe greater-than-or-equal

| xref:utilities.adoc#in_range[`in_range`]
| Whether a value is representable in a target integer type
|===

[#api_macros]
Expand Down
94 changes: 94 additions & 0 deletions doc/modules/ROOT/pages/utilities.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,97 @@ ckd_add<int64_t>(uint128_t{5}, int128_t{-3}): overflow=false, result=2
ckd_mul<uint8_t>(20, 20): overflow=true, wrapped=144
----
====

[#comparison]
== Integer Comparison

`cmp_equal`, `cmp_not_equal`, `cmp_less`, `cmp_greater`, `cmp_less_equal`, and `cmp_greater_equal` are the C++26 https://eel.is/c++draft/utility.intcmp[integer comparison functions] extended to the library and builtin 128-bit types.
They are available in C++14 and later.

Unlike the built-in relational operators, which follow the usual arithmetic conversions, these functions compare the true mathematical values of their operands regardless of signedness.
When a `uint128_t` is compared with an `int128_t`, the operators convert the signed operand to unsigned, so `int128_t{-1} < uint128_t{0}` is reported as `false` and `(std::numeric_limits<uint128_t>::max)() == int128_t{-1}` as `true`.
The `cmp_*` functions report the mathematically correct answer in both cases.

Throughout this section a *128-bit type* is `int128_t`, `uint128_t`, or a builtin 128-bit type where the platform provides one (`+__int128+` and its unsigned counterpart, or `std::_Signed128` and `std::_Unsigned128` on MSVC).

[source, c++]
----
namespace boost {
namespace int128 {

// Each function participates in overload resolution only when both operands are
// permitted and at least one is a 128-bit type. The other operand may be a
// 128-bit type or any standard or extended integer type other than bool, a
// character type, or std::byte.

template <typename T, typename U>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_equal(T a, U b) noexcept;
template <typename T, typename U>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_not_equal(T a, U b) noexcept;
template <typename T, typename U>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_less(T a, U b) noexcept;
template <typename T, typename U>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_greater(T a, U b) noexcept;
template <typename T, typename U>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_less_equal(T a, U b) noexcept;
template <typename T, typename U>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_greater_equal(T a, U b) noexcept;

} // namespace int128
} // namespace boost
----

`cmp_equal(a, b)` returns `true` when `a` and `b` are mathematically equal, and `cmp_less(a, b)` returns `true` when `a` is mathematically less than `b`.
The remaining four functions are defined in terms of these two exactly as in the standard: `cmp_not_equal` is `!cmp_equal(a, b)`, `cmp_greater(a, b)` is `cmp_less(b, a)`, `cmp_less_equal(a, b)` is `!cmp_less(b, a)`, and `cmp_greater_equal(a, b)` is `!cmp_less(a, b)`.

Every operand pair in which at least one operand is a 128-bit type is supported, including a `uint128_t` (or an unsigned builtin 128-bit value) against a signed `int128_t` (or a signed builtin one).
Following the standard, a `bool`, a character type (`char`, `wchar_t`, `char8_t`, `char16_t`, `char32_t`), or `std::byte` is not a permitted operand, and such a call is ill-formed.

[#in_range]
== Range Checking

`in_range<R>(t)` returns `true` when the value `t` is representable in the type `R`, that is, when `R` can hold `t` without the value changing.
It is equivalent to `cmp_greater_equal(t, (std::numeric_limits<R>::min)()) && cmp_less_equal(t, (std::numeric_limits<R>::max)())`, so the range check is itself signedness-safe.

[source, c++]
----
namespace boost {
namespace int128 {

template <typename R, typename Integer>
BOOST_INT128_HOST_DEVICE constexpr bool in_range(Integer t) noexcept;

} // namespace int128
} // namespace boost
----

The target type `R` and the type of `t` may each be a builtin integer or a 128-bit type; at least one of them is a 128-bit type (use `std::in_range` when neither is).
For example, `in_range<std::uint8_t>(int128_t{-1})` is `false` because a negative value is not representable in an unsigned type, and `in_range<std::int64_t>((std::numeric_limits<int128_t>::max)())` is `false` because the value is too large.

.This https://github.com/cppalliance/int128/blob/develop/examples/integer_comparison.cpp[example] demonstrates signedness-safe comparison and range checking with the library 128-bit types
====
[source, c++]
----
include::example$integer_comparison.cpp[]
----

.Expected Output
[listing]
----
=== Signedness-safe comparison ===
UINT128_MAX == int128_t{-1} (operator): true
cmp_equal(UINT128_MAX, int128_t{-1}): false
int128_t{-1} < uint128_t{0} (operator): false
cmp_less(int128_t{-1}, uint128_t{0}): true

=== Mixed with builtin integers ===
cmp_less(int128_t{-5}, 0u): true
cmp_equal(uint128_t{42}, 42): true

=== in_range ===
in_range<std::int8_t>(int128_t{200}): false
in_range<std::uint8_t>(int128_t{-1}): false
in_range<std::uint64_t>(UINT128_MAX): false
in_range<int128_t>(UINT128_MAX): false
----
====
54 changes: 54 additions & 0 deletions examples/integer_comparison.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2026 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

// Individual headers

#include <boost/int128/utilities.hpp>
#include <boost/int128/iostream.hpp>

// Or you can do a single header

// #include <boost/int128.hpp>

#include <cstdint>
#include <limits>
#include <iostream>

int main()
{
using boost::int128::uint128_t;
using boost::int128::int128_t;
using boost::int128::cmp_equal;
using boost::int128::cmp_less;
using boost::int128::in_range;

std::cout << std::boolalpha;

constexpr auto u_max {(std::numeric_limits<uint128_t>::max)()};

// The built-in relational operators follow the usual arithmetic conversions,
// so comparing a uint128_t with a negative int128_t converts the negative
// value to a huge unsigned one and gives the wrong answer. The cmp_* family
// compares the true mathematical values instead.
std::cout << "=== Signedness-safe comparison ===" << std::endl;
std::cout << "UINT128_MAX == int128_t{-1} (operator): " << (u_max == int128_t{-1}) << std::endl;
std::cout << "cmp_equal(UINT128_MAX, int128_t{-1}): " << cmp_equal(u_max, int128_t{-1}) << std::endl;
std::cout << "int128_t{-1} < uint128_t{0} (operator): " << (int128_t{-1} < uint128_t{0}) << std::endl;
std::cout << "cmp_less(int128_t{-1}, uint128_t{0}): " << cmp_less(int128_t{-1}, uint128_t{0}) << std::endl;

// Either operand may be a builtin integer of any width and signedness.
std::cout << "\n=== Mixed with builtin integers ===" << std::endl;
std::cout << "cmp_less(int128_t{-5}, 0u): " << cmp_less(int128_t{-5}, 0U) << std::endl;
std::cout << "cmp_equal(uint128_t{42}, 42): " << cmp_equal(uint128_t{42}, 42) << std::endl;

// in_range<R>(v) reports whether v is representable in the target type R.
// R and the type of v may each be a builtin integer or a 128-bit type.
std::cout << "\n=== in_range ===" << std::endl;
std::cout << "in_range<std::int8_t>(int128_t{200}): " << in_range<std::int8_t>(int128_t{200}) << std::endl;
std::cout << "in_range<std::uint8_t>(int128_t{-1}): " << in_range<std::uint8_t>(int128_t{-1}) << std::endl;
std::cout << "in_range<std::uint64_t>(UINT128_MAX): " << in_range<std::uint64_t>(u_max) << std::endl;
std::cout << "in_range<int128_t>(UINT128_MAX): " << in_range<int128_t>(u_max) << std::endl;

return 0;
}
155 changes: 155 additions & 0 deletions include/boost/int128/utilities.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#ifndef BOOST_INT128_BUILD_MODULE

#include <cstddef>
#include <cstdint>
#include <limits>
#include <type_traits>
Expand Down Expand Up @@ -432,6 +433,160 @@ BOOST_INT128_HOST_DEVICE constexpr bool ckd_mul(T1* result, const T2 a, const T3
return detail::ckd_overflows<T1>(product_magnitude, product_negative, exceeds_width);
}

namespace detail {

// See: https://eel.is/c++draft/utility.intcmp
// [Note 1: These function templates cannot be used to compare byte, char, char8_t, char16_t, char32_t, wchar_t, and bool. end note]
template <typename T>
struct valid_comparison_type
{
static constexpr bool value = std::is_integral<T>::value &&
!std::is_same<T, char>::value &&
!std::is_same<T, char16_t>::value &&
!std::is_same<T, char32_t>::value &&
!std::is_same<T, wchar_t>::value &&
!std::is_same<T, bool>::value
#if defined(__cpp_char8_t)
&& !std::is_same<T, char8_t>::value
#endif
#if defined(__cpp_lib_byte) && __cpp_lib_byte >= 201603L
&& !std::is_same<T, std::byte>::value
#endif
;
};

template <typename T>
BOOST_INT128_INLINE_CONSTEXPR bool is_valid_comparison_type_v = valid_comparison_type<T>::value;

// Allow the builtins to be used when available
template <typename T>
BOOST_INT128_INLINE_CONSTEXPR bool is_int128_type_v = std::is_same<T, int128_t>::value ||
std::is_same<T, uint128_t>::value
#if defined(BOOST_INT128_HAS_INT128) || defined(BOOST_INT128_HAS_MSVC_INT128)
|| std::is_same<T, builtin_i128>::value
|| std::is_same<T, builtin_u128>::value
#endif
;

template <typename T>
BOOST_INT128_INLINE_CONSTEXPR bool is_valid_comparison_operand_v = is_valid_comparison_type_v<T> ||
is_int128_type_v<T>;

// Maps the builtin 128-bit types onto the library equivalents
template <typename T>
struct comparison_canonical
{
using type = T;
};

#if defined(BOOST_INT128_HAS_INT128) || defined(BOOST_INT128_HAS_MSVC_INT128)

template <>
struct comparison_canonical<builtin_i128>
{
using type = int128_t;
};

template <>
struct comparison_canonical<builtin_u128>
{
using type = uint128_t;
};

#endif

template <typename T>
using comparison_canonical_t = typename comparison_canonical<T>::type;

template <typename T>
BOOST_INT128_HOST_DEVICE constexpr comparison_canonical_t<T> canonical_comparison_operand(const T value) noexcept
{
return static_cast<comparison_canonical_t<T>>(value);
}

// Mathematical equality of two integers regardless of their signedness, via the
// same (sign, magnitude) decomposition.
template <typename T, typename U>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_equal_impl(const T lhs, const U rhs) noexcept
{
const auto a {ckd_decompose(canonical_comparison_operand(lhs))};
const auto b {ckd_decompose(canonical_comparison_operand(rhs))};

return (a.negative == b.negative) && (a.magnitude == b.magnitude);
}

// Mathematical less-than of two integers regardless of their signedness, via the
// same (sign, magnitude) decomposition.
template <typename T, typename U>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_less_impl(const T lhs, const U rhs) noexcept
{
const auto a {ckd_decompose(canonical_comparison_operand(lhs))};
const auto b {ckd_decompose(canonical_comparison_operand(rhs))};

if (a.negative != b.negative)
{
return a.negative;
}

return a.negative ? (a.magnitude > b.magnitude) : (a.magnitude < b.magnitude);
}

template <typename T, typename U>
BOOST_INT128_INLINE_CONSTEXPR bool enable_comparison_v = is_valid_comparison_operand_v<T> &&
is_valid_comparison_operand_v<U> &&
(is_int128_type_v<T> || is_int128_type_v<U>);

} // namespace detail

// C++26 integer comparison functions (https://eel.is/c++draft/utility.intcmp)
// extended to the library and builtin 128-bit types and available from C++14.

BOOST_INT128_EXPORT template <typename T, typename U, std::enable_if_t<detail::enable_comparison_v<T, U>, bool> = true>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_equal(const T lhs, const U rhs) noexcept
{
return detail::cmp_equal_impl(lhs, rhs);
}

BOOST_INT128_EXPORT template <typename T, typename U, std::enable_if_t<detail::enable_comparison_v<T, U>, bool> = true>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_not_equal(const T lhs, const U rhs) noexcept
{
return !detail::cmp_equal_impl(lhs, rhs);
}

BOOST_INT128_EXPORT template <typename T, typename U, std::enable_if_t<detail::enable_comparison_v<T, U>, bool> = true>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_less(const T lhs, const U rhs) noexcept
{
return detail::cmp_less_impl(lhs, rhs);
}

BOOST_INT128_EXPORT template <typename T, typename U, std::enable_if_t<detail::enable_comparison_v<T, U>, bool> = true>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_greater(const T lhs, const U rhs) noexcept
{
return detail::cmp_less_impl(rhs, lhs);
}

BOOST_INT128_EXPORT template <typename T, typename U, std::enable_if_t<detail::enable_comparison_v<T, U>, bool> = true>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_less_equal(const T lhs, const U rhs) noexcept
{
return !detail::cmp_less_impl(rhs, lhs);
}

BOOST_INT128_EXPORT template <typename T, typename U, std::enable_if_t<detail::enable_comparison_v<T, U>, bool> = true>
BOOST_INT128_HOST_DEVICE constexpr bool cmp_greater_equal(const T lhs, const U rhs) noexcept
{
return !detail::cmp_less_impl(lhs, rhs);
}

// Whether t is representable in the target type R.
BOOST_INT128_EXPORT template <typename R, typename T, std::enable_if_t<detail::enable_comparison_v<R, T>, bool> = true>
BOOST_INT128_HOST_DEVICE constexpr bool in_range(const T t) noexcept
{
using limits = std::numeric_limits<detail::comparison_canonical_t<R>>;

return !detail::cmp_less_impl(t, (limits::min)()) &&
!detail::cmp_less_impl((limits::max)(), t);
}

} // namespace int128
} // namespace boost

Expand Down
3 changes: 3 additions & 0 deletions test/Jamfile
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ run test_powm.cpp ;
run test_ipow.cpp ;
run test_isqrt.cpp ;
run test_ckd.cpp ;
run test_integer_comparison.cpp ;

run test_format.cpp ;
run test_fmt_format.cpp ;
Expand All @@ -113,6 +114,7 @@ run ../examples/construction.cpp ;
run ../examples/bit.cpp ;
run ../examples/saturating_arithmetic.cpp ;
run ../examples/checked_arithmetic.cpp ;
run ../examples/integer_comparison.cpp ;
run ../examples/mixed_type_arithmetic.cpp ;
run ../examples/stream.cpp ;
run ../examples/basic_arithmetic.cpp ;
Expand Down Expand Up @@ -157,3 +159,4 @@ compile compile_tests/utilities_compile.cpp ;
compile-fail compile_tests/literals_overflow_fail.cpp ;
compile-fail compile_tests/literals_base_prefix_fail.cpp ;
compile-fail compile_tests/literals_invalid_fail.cpp ;
compile-fail compile_tests/utilities_comparison_fail.cpp ;
13 changes: 13 additions & 0 deletions test/compile_tests/utilities_comparison_fail.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright 2026 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

// The C++26 integer comparison functions reject plain char (and the other
// character types, bool, and std::byte). This must fail to compile.

#include <boost/int128/utilities.hpp>

int main()
{
return boost::int128::cmp_equal(boost::int128::uint128_t{1}, 'a') ? 1 : 0;
}
Loading
Loading