Ben
Ben

Reputation: 9713

C++ function to safely compare integers of different types?

Due to implicit conversion and the fact that std::numeric_limits<T>::max() and friends return type T, it seems non-trivial to write a function bool cmp(IntA a, IntB b) that "does the right thing" to conceptually return a < b;. That is, if they share a common range, compare, if not, determine if a is less than b regardless of number of bits or signedness. Is there a simpler implementation than this naive one?:

template <typename IntA, typename IntB>
[[nodiscard]] constexpr bool cmp(IntA a, IntB b) noexcept {
    static_assert(std::is_integral_v<IntA>);
    static_assert(sizeof(IntA) <= sizeof(long long int), "We assume we can fit everything into long long");
    static_assert(std::is_integral_v<IntB>);
    static_assert(sizeof(IntB) <= sizeof(long long int), "We assume we can fit everything into long long");
    if (a < 0) {
        if (b < 0) {
            return static_cast<signed long long int>(a) < static_cast<signed long long int>(b);
        } else {
            return true;
        }
    } else {
        if (b < 0) {
            return false;
        } else {
            return static_cast<unsigned long long int>(a) < static_cast<unsigned long long int>(b);
        }
    }
}

https://godbolt.org/z/aPbozGW9j

And what can we do to ensure it gets fully optimized?

Upvotes: 4

Views: 1389

Answers (3)

A.Hristov
A.Hristov

Reputation: 483

How about this

template <typename IntA, typename IntB>
bool cmp(IntA a, IntB b) { 
    if constexpr (std::is_signed<IntA>::value == std::is_signed<IntB>::value)
    {
        return a < b;
    }
    return a < 0 || (b >= 0 && a < b);
}

If both types are of the same signed-ness, then comparing them is safe. If they are not, then:

  1. if a < 0 then b >= 0 and we return true.
  2. otherwise a >= 0. If b < 0 the expression will return false. If b >= 0 then both are positive and it's safe to return a < b.

https://godbolt.org/z/hPKxoscKn

Upvotes: 1

NathanOliver
NathanOliver

Reputation: 180945

If you can use C++20, then you can use the newly added functions to do just this. We now have

template< class T, class U >
constexpr bool cmp_equal( T t, U u ) noexcept;

template< class T, class U >
constexpr bool cmp_not_equal( T t, U u ) noexcept;

template< class T, class U >
constexpr bool cmp_less( T t, U u ) noexcept;

template< class T, class U >
constexpr bool cmp_greater( T t, U u ) noexcept;

template< class T, class U >
constexpr bool cmp_less_equal( T t, U u ) noexcept;

template< class T, class U >
constexpr bool cmp_greater_equal( T t, U u ) noexcept;

which

Compare the values of two integers t and u. Unlike builtin comparison operators, negative signed integers always compare less than (and not equal to) unsigned integers: the comparison is safe against lossy integer conversion.

-1 > 0u; // true
std::cmp_greater(-1, 0u); // false

It is a compile-time error if either T or U is not a signed or unsigned integer type (including standard integer type and extended integer type).

Upvotes: 8

Mike Vine
Mike Vine

Reputation: 9837

Your jumping through hoops at runtime (via comparing with zero) is only necessary when the signed'ness of the inputs mismatch. So I'd recommend using if constexpr to help the compiler get it right in only the cases where it might be useful. Something like:

template <typename LargestSignedInt = long long int, typename IntA, typename IntB>
[[nodiscard]] constexpr bool cmp(IntA a, IntB b) noexcept {
    static_assert(std::is_integral_v<IntA>);
    static_assert(sizeof(IntA) <= sizeof(LargestSignedInt));
    static_assert(std::is_integral_v<IntB>);
    static_assert(sizeof(IntB) <= sizeof(LargestSignedInt));

    if constexpr(sizeof(IntA) < sizeof(LargestSignedInt) && sizeof(IntB) < sizeof(LargestSignedInt))
    {
        // Both types are smaller than largest signed type supported, just use largest signed type.
        return static_cast<LargestSignedInt>(a) < static_cast<LargestSignedInt>(b);
    }

    if constexpr(std::is_signed_v<IntA> != std::is_signed_v<IntB>)
    {
        // Signedness mismatch
        if (a < 0) {
            if (b < 0) {
                return static_cast<LargestSignedInt>(a) < static_cast<LargestSignedInt>(b);
            } else {
                return true;
            }
        } else {
            if (b < 0) {
                return false;
            } else {
                return static_cast<typename std::make_unsigned<LargestSignedInt>::type>(a) < static_cast<typename std::make_unsigned<LargestSignedInt>::type>(b);
            }
        }
    }

    if constexpr(std::is_signed_v<IntA>)
    {
        // Both types are signed.
        return static_cast<LargestSignedInt>(a) < static_cast<LargestSignedInt>(b);
    }

    // Both types are unsigned.
    return static_cast<typename std::make_unsigned<LargestSignedInt>::type>(a) < static_cast<typename std::make_unsigned<LargestSignedInt>::type>(b);
}

Upvotes: 0

Related Questions