Add safe integer comparisons (#1506)

Adding cmp_equal, cmp_not_equal, cmp_less, cmp_greater, cmp_less_equal,
cmp_greater_equal and in_range.

Co-authored-by: John Wellbelove <jwellbelove@users.noreply.github.com>
This commit is contained in:
Roland Reichwein 2026-07-18 16:37:53 +02:00 committed by GitHub
parent 66f904b1a3
commit 49f690a921
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 399 additions and 0 deletions

View File

@ -71,6 +71,82 @@ T exchange(T& object, const U& new_value)
Copies the new value to object and returns the old value. Copies the new value to object and returns the old value.
Note: This is not an atomic operation. Note: This is not an atomic operation.
## Integer comparison functions
Safe comparison of integers of different signedness. Unlike the built-in
comparison operators, negative signed integers always compare less than (and
never equal to) unsigned integers, so the result is never affected by
value-changing implicit conversions.
The arguments must be standard signed or unsigned integer types, including
`signed char` and `unsigned char`. `bool`, `char` and the extended character
types are not permitted.
When the STL is available and the compiler provides
`__cpp_lib_integer_comparison_functions` (C++20), the `std` versions are used.
Since: `20.49.0`
```cpp
template <typename T, typename U>
constexpr bool cmp_equal(T t, U u) noexcept
```
**Description**
Returns `true` if `t` is equal to `u`.
---
```cpp
template <typename T, typename U>
constexpr bool cmp_not_equal(T t, U u) noexcept
```
**Description**
Returns `true` if `t` is not equal to `u`.
---
```cpp
template <typename T, typename U>
constexpr bool cmp_less(T t, U u) noexcept
```
**Description**
Returns `true` if `t` is less than `u`.
---
```cpp
template <typename T, typename U>
constexpr bool cmp_greater(T t, U u) noexcept
```
**Description**
Returns `true` if `t` is greater than `u`.
---
```cpp
template <typename T, typename U>
constexpr bool cmp_less_equal(T t, U u) noexcept
```
**Description**
Returns `true` if `t` is less than or equal to `u`.
---
```cpp
template <typename T, typename U>
constexpr bool cmp_greater_equal(T t, U u) noexcept
```
**Description**
Returns `true` if `t` is greater than or equal to `u`.
---
```cpp
template <typename R, typename T>
constexpr bool in_range(T t) noexcept
```
**Description**
Returns `true` if the value of `t` is in the range of values that can be
represented by `R`, that is, if `t` can be converted to `R` without loss of its
value.
## add_const ## add_const
```cpp ```cpp

View File

@ -32,6 +32,7 @@ SOFTWARE.
#define ETL_UTILITY_INCLUDED #define ETL_UTILITY_INCLUDED
#include "platform.h" #include "platform.h"
#include "limits.h"
#include "type_traits.h" #include "type_traits.h"
#include "private/tuple_element.h" #include "private/tuple_element.h"
@ -142,6 +143,173 @@ namespace etl
return static_cast<typename underlying_type<T>::type>(value); return static_cast<typename underlying_type<T>::type>(value);
} }
#if ETL_USING_STL && ETL_USING_CPP20 && defined(__cpp_lib_integer_comparison_functions)
//***************************************************************************
// Safe integer comparison functions.
//***************************************************************************
using std::cmp_equal;
using std::cmp_greater;
using std::cmp_greater_equal;
using std::cmp_less;
using std::cmp_less_equal;
using std::cmp_not_equal;
using std::in_range;
#else
namespace private_utility
{
//***********************************
/// Determines whether T is one of the standard integer types permitted as
/// an argument to the safe integer comparison functions. The permitted
/// types are the signed and unsigned standard integer types, including
/// signed char and unsigned char. bool, char and the extended character
/// types are excluded, matching the C++ standard's constraints on
/// etl::cmp_* and etl::in_range.
//***********************************
template <typename T>
struct is_standard_integer : etl::false_type
{
};
template <>
struct is_standard_integer<signed char> : etl::true_type
{
};
template <>
struct is_standard_integer<short> : etl::true_type
{
};
template <>
struct is_standard_integer<int> : etl::true_type
{
};
template <>
struct is_standard_integer<long> : etl::true_type
{
};
template <>
struct is_standard_integer<long long> : etl::true_type
{
};
template <>
struct is_standard_integer<unsigned char> : etl::true_type
{
};
template <>
struct is_standard_integer<unsigned short> : etl::true_type
{
};
template <>
struct is_standard_integer<unsigned int> : etl::true_type
{
};
template <>
struct is_standard_integer<unsigned long> : etl::true_type
{
};
template <>
struct is_standard_integer<unsigned long long> : etl::true_type
{
};
template <typename T>
struct is_standard_integer<const T> : is_standard_integer<T>
{
};
template <typename T>
struct is_standard_integer<volatile T> : is_standard_integer<T>
{
};
template <typename T>
struct is_standard_integer<const volatile T> : is_standard_integer<T>
{
};
//***********************************
/// Enabled when both T and U are permitted comparison types.
//***********************************
template <typename T, typename U, typename TReturn = void>
struct enable_if_comparable : etl::enable_if<is_standard_integer<T>::value && is_standard_integer<U>::value, TReturn>
{
};
} // namespace private_utility
//***************************************************************************
/// Compares the values of two integers.
/// Unlike the built-in comparison operators, negative signed integers always
/// compare less than (and not equal to) unsigned integers.
/// The comparison is safe against value-changing implicit conversions.
/// https://en.cppreference.com/w/cpp/utility/intcmp
///\ingroup utility
//***************************************************************************
template <typename T, typename U>
ETL_NODISCARD ETL_CONSTEXPR typename private_utility::enable_if_comparable<T, U, bool>::type cmp_equal(T t, U u) ETL_NOEXCEPT
{
return (etl::is_signed<T>::value == etl::is_signed<U>::value) ? (t == u)
: etl::is_signed<T>::value ? ((t >= T(0)) && (typename etl::make_unsigned<T>::type(t) == u))
: ((u >= U(0)) && (t == typename etl::make_unsigned<U>::type(u)));
}
//***************************************************************************
/// See @ref cmp_equal.
//***************************************************************************
template <typename T, typename U>
ETL_NODISCARD ETL_CONSTEXPR typename private_utility::enable_if_comparable<T, U, bool>::type cmp_not_equal(T t, U u) ETL_NOEXCEPT
{
return !etl::cmp_equal(t, u);
}
//***************************************************************************
/// See @ref cmp_equal.
//***************************************************************************
template <typename T, typename U>
ETL_NODISCARD ETL_CONSTEXPR typename private_utility::enable_if_comparable<T, U, bool>::type cmp_less(T t, U u) ETL_NOEXCEPT
{
return (etl::is_signed<T>::value == etl::is_signed<U>::value) ? (t < u)
: etl::is_signed<T>::value ? ((t < T(0)) || (typename etl::make_unsigned<T>::type(t) < u))
: ((u >= U(0)) && (t < typename etl::make_unsigned<U>::type(u)));
}
//***************************************************************************
/// See @ref cmp_equal.
//***************************************************************************
template <typename T, typename U>
ETL_NODISCARD ETL_CONSTEXPR typename private_utility::enable_if_comparable<T, U, bool>::type cmp_greater(T t, U u) ETL_NOEXCEPT
{
return etl::cmp_less(u, t);
}
//***************************************************************************
/// See @ref cmp_equal.
//***************************************************************************
template <typename T, typename U>
ETL_NODISCARD ETL_CONSTEXPR typename private_utility::enable_if_comparable<T, U, bool>::type cmp_less_equal(T t, U u) ETL_NOEXCEPT
{
return !etl::cmp_less(u, t);
}
//***************************************************************************
/// See @ref cmp_equal.
//***************************************************************************
template <typename T, typename U>
ETL_NODISCARD ETL_CONSTEXPR typename private_utility::enable_if_comparable<T, U, bool>::type cmp_greater_equal(T t, U u) ETL_NOEXCEPT
{
return !etl::cmp_less(t, u);
}
//***************************************************************************
/// Returns whether the value of t is in the range of values that can be
/// represented by R, that is, whether t can be converted to R without loss
/// of its value.
/// https://en.cppreference.com/w/cpp/utility/in_range
///\ingroup utility
//***************************************************************************
template <typename R, typename T>
ETL_NODISCARD ETL_CONSTEXPR typename private_utility::enable_if_comparable<R, T, bool>::type in_range(T t) ETL_NOEXCEPT
{
return etl::cmp_greater_equal(t, etl::numeric_limits<R>::min()) && etl::cmp_less_equal(t, etl::numeric_limits<R>::max());
}
#endif
// We can't have std::swap and etl::swap templates coexisting in the unit // We can't have std::swap and etl::swap templates coexisting in the unit
// tests as the compiler will be unable to decide which one to use, due to // tests as the compiler will be unable to decide which one to use, due to
// ADL. // ADL.

View File

@ -785,6 +785,161 @@ namespace
} }
#endif #endif
//*********************************
TEST(test_cmp_equal)
{
// Same signedness.
CHECK_TRUE(etl::cmp_equal(1, 1));
CHECK_FALSE(etl::cmp_equal(1, 2));
CHECK_TRUE(etl::cmp_equal(1U, 1U));
CHECK_FALSE(etl::cmp_equal(1U, 2U));
// Mixed signedness where the built-in operator would give the wrong result.
// -1 as unsigned would be a large value, so a naive (a == b) would be false anyway,
// but the classic trap is comparing a negative signed with an equal-bit-pattern unsigned.
CHECK_FALSE(etl::cmp_equal(-1, static_cast<unsigned int>(-1)));
CHECK_FALSE(etl::cmp_equal(static_cast<unsigned int>(-1), -1));
// Non-negative signed compared with unsigned.
CHECK_TRUE(etl::cmp_equal(5, 5U));
CHECK_TRUE(etl::cmp_equal(5U, 5));
CHECK_FALSE(etl::cmp_equal(-5, 5U));
CHECK_FALSE(etl::cmp_equal(5U, -5));
// Different widths.
CHECK_TRUE(etl::cmp_equal(int8_t(100), int64_t(100)));
CHECK_FALSE(etl::cmp_equal(int8_t(-1), uint64_t(0xFFFFFFFFFFFFFFFFULL)));
}
//*********************************
TEST(test_cmp_not_equal)
{
CHECK_FALSE(etl::cmp_not_equal(1, 1));
CHECK_TRUE(etl::cmp_not_equal(1, 2));
CHECK_TRUE(etl::cmp_not_equal(-1, static_cast<unsigned int>(-1)));
CHECK_FALSE(etl::cmp_not_equal(5, 5U));
CHECK_TRUE(etl::cmp_not_equal(-5, 5U));
}
//*********************************
TEST(test_cmp_less)
{
// Same signedness.
CHECK_TRUE(etl::cmp_less(1, 2));
CHECK_FALSE(etl::cmp_less(2, 1));
CHECK_FALSE(etl::cmp_less(1, 1));
CHECK_TRUE(etl::cmp_less(1U, 2U));
CHECK_FALSE(etl::cmp_less(2U, 1U));
// A negative signed value is always less than any unsigned value.
CHECK_TRUE(etl::cmp_less(-1, 0U));
CHECK_TRUE(etl::cmp_less(-1, static_cast<unsigned int>(-1)));
CHECK_FALSE(etl::cmp_less(static_cast<unsigned int>(-1), -1));
// Non-negative signed with unsigned.
CHECK_TRUE(etl::cmp_less(5, 6U));
CHECK_FALSE(etl::cmp_less(6, 5U));
CHECK_TRUE(etl::cmp_less(5U, 6));
CHECK_FALSE(etl::cmp_less(6U, 5));
// Large unsigned is not less than a small signed.
CHECK_FALSE(etl::cmp_less(0xFFFFFFFFU, 1));
CHECK_TRUE(etl::cmp_less(1, 0xFFFFFFFFU));
}
//*********************************
TEST(test_cmp_greater)
{
CHECK_TRUE(etl::cmp_greater(2, 1));
CHECK_FALSE(etl::cmp_greater(1, 2));
CHECK_FALSE(etl::cmp_greater(1, 1));
CHECK_FALSE(etl::cmp_greater(-1, 0U));
CHECK_TRUE(etl::cmp_greater(static_cast<unsigned int>(-1), -1));
CHECK_TRUE(etl::cmp_greater(0xFFFFFFFFU, 1));
CHECK_FALSE(etl::cmp_greater(1, 0xFFFFFFFFU));
}
//*********************************
TEST(test_cmp_less_equal)
{
CHECK_TRUE(etl::cmp_less_equal(1, 1));
CHECK_TRUE(etl::cmp_less_equal(1, 2));
CHECK_FALSE(etl::cmp_less_equal(2, 1));
CHECK_TRUE(etl::cmp_less_equal(-1, static_cast<unsigned int>(-1)));
CHECK_FALSE(etl::cmp_less_equal(static_cast<unsigned int>(-1), -1));
CHECK_TRUE(etl::cmp_less_equal(5U, 5));
CHECK_FALSE(etl::cmp_less_equal(6U, 5));
}
//*********************************
TEST(test_cmp_greater_equal)
{
CHECK_TRUE(etl::cmp_greater_equal(1, 1));
CHECK_TRUE(etl::cmp_greater_equal(2, 1));
CHECK_FALSE(etl::cmp_greater_equal(1, 2));
CHECK_FALSE(etl::cmp_greater_equal(-1, static_cast<unsigned int>(-1)));
CHECK_TRUE(etl::cmp_greater_equal(static_cast<unsigned int>(-1), -1));
CHECK_TRUE(etl::cmp_greater_equal(5, 5U));
CHECK_FALSE(etl::cmp_greater_equal(-5, 5U));
}
//*********************************
TEST(test_in_range)
{
// Signed target type.
CHECK_TRUE(etl::in_range<int8_t>(0));
CHECK_TRUE(etl::in_range<int8_t>(127));
CHECK_TRUE(etl::in_range<int8_t>(-128));
CHECK_FALSE(etl::in_range<int8_t>(128));
CHECK_FALSE(etl::in_range<int8_t>(-129));
CHECK_FALSE(etl::in_range<int8_t>(200));
// Unsigned target type.
CHECK_TRUE(etl::in_range<uint8_t>(0));
CHECK_TRUE(etl::in_range<uint8_t>(255));
CHECK_FALSE(etl::in_range<uint8_t>(256));
CHECK_FALSE(etl::in_range<uint8_t>(-1));
// Unsigned source that would be negative if reinterpreted as signed.
CHECK_FALSE(etl::in_range<int8_t>(static_cast<unsigned int>(-1)));
CHECK_TRUE(etl::in_range<uint32_t>(static_cast<unsigned int>(-1)));
// Wider target always contains a narrower value.
CHECK_TRUE(etl::in_range<int64_t>(int8_t(-1)));
CHECK_TRUE(etl::in_range<int64_t>(uint32_t(0xFFFFFFFFU)));
}
#if ETL_USING_CPP14
//*********************************
TEST(test_cmp_and_in_range_constexpr)
{
constexpr bool ce0 = etl::cmp_equal(1, 1);
constexpr bool ce1 = etl::cmp_not_equal(-1, static_cast<unsigned int>(-1));
constexpr bool ce2 = etl::cmp_less(-1, 0U);
constexpr bool ce3 = etl::cmp_greater(static_cast<unsigned int>(-1), -1);
constexpr bool ce4 = etl::cmp_less_equal(-1, static_cast<unsigned int>(-1));
constexpr bool ce5 = etl::cmp_greater_equal(static_cast<unsigned int>(-1), -1);
constexpr bool ce6 = etl::in_range<int8_t>(100);
constexpr bool ce7 = etl::in_range<int8_t>(200);
static_assert(ce0, "cmp_equal constexpr");
static_assert(ce1, "cmp_not_equal constexpr");
static_assert(ce2, "cmp_less constexpr");
static_assert(ce3, "cmp_greater constexpr");
static_assert(ce4, "cmp_less_equal constexpr");
static_assert(ce5, "cmp_greater_equal constexpr");
static_assert(ce6, "in_range true constexpr");
static_assert(!ce7, "in_range false constexpr");
CHECK_TRUE(ce0);
CHECK_TRUE(ce6);
CHECK_FALSE(ce7);
}
#endif
#if ETL_HAS_PACKED #if ETL_HAS_PACKED
//********************************* //*********************************
TEST(test_packed) TEST(test_packed)