Search code examples
c++operator-overloadingc++20customization-point

How can I write a generic equality comparison that will fall back to operator== when std::cmp_equal won't compile?


I want a generic equality-checking function that prefers to use std::cmp_equal when possible because of its virtues, but will use operator== for types that std::cmp_equal can't handle, such as bool or user-defined types.

This seemed like it should be an easy job for requires, but I was surprised to discover that although this works for libc++, it fails for libstdc++ with static_assert messages.

#include <utility>

template <typename T, typename U>
auto generic_equal(T t, U u) {
    if constexpr (requires { std::cmp_equal(t, u); }) {
        return std::cmp_equal(t, u);
    } else {
        return t == u;
    }
}

struct A {
    friend bool operator==(A,A) = default;
};

int main() {
    generic_equal(false, false);
    generic_equal(-1, 5);
    generic_equal(A{}, A{});
}

Is there a good way to convince the compiler to realize it can't instantiate std::cmp_equal at the requires instead of at the call?

Compiler explorer link showing success with libc++ and failure with libstdc++: https://godbolt.org/z/nKzfE87Ye


Solution

  • std::cmp_equal requires that the template parameter must be of standard integer types or extended integer types, which is defined in [basic.fundamental]:

    1. There are five standard signed integer types: “signed char”, “short int”, “int”, “long int”, and “long long int”. In this list, each type provides at least as much storage as those preceding it in the list. There may also be implementation-defined extended signed integer types. [...]

    2. For each of the standard signed integer types, there exists a corresponding (but different) standard unsigned integer type: “unsigned char”, “unsigned short int”, “unsigned int”, “unsigned long int”, and “unsigned long long int”. Likewise, for each of the extended signed integer types, there exists a corresponding extended unsigned integer type. [...]

    Since it is not a constrained function, it will still be instantiated and hit internal possible static assertions when passed a non-standard integer type. You can determine whether to use std::cmp_equal for comparison by detecting whether the T is a standard integer type:

    #include <utility>
    #include <concepts>
    
    template <typename T>
    concept standard_integral = 
      std::integral<T> &&
      !std::same_as<T, char> &&
      !std::same_as<T, char8_t> &&
      !std::same_as<T, char16_t> &&
      !std::same_as<T, char32_t> &&
      !std::same_as<T, wchar_t> &&
      !std::same_as<T, bool>;
    
    template <typename T, std::equality_comparable_with<T> U>
    bool generic_equal(T t, U u) {
        if constexpr (standard_integral<T> && standard_integral<U>) {
            return std::cmp_equal(t, u);
        } else {
            return t == u;
        }
    }