6
\$\begingroup\$

I started to write some algorithms that work on heterogeneous container std::tuple. At some point I needed a way to determine whether or not the types that are contained by the tuple are comparable to each other (and whether that comparison yields a result that is convertible to a user defined type), so I wrote a type trait for checking that. You can also explicitly check if a specific comparison operator exists for the given 2 types.

I just wanted to get some feedback on the implementation and possible concerns, issues etc.

For each type trait there is also a helper variable template, as that's usual for type traits in the standard library since C++14.

Note: This could have also been achieved using C++11, but because I am using C++17 in my algorithms I might as well use it in here. :)

#include <type_traits>
#include <utility>

namespace meta
{
    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U, typename = std::void_t<>>
    struct has_equal
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_equal_r
        : std::false_type
    {};

    template<typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_equal
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_equal_r
        : std::false_type
    {};

    template<typename T, typename U>
    struct has_equal<T, U, std::void_t<decltype(std::declval<T>() == std::declval<U>())>>
        : std::true_type
    {};

    template<typename R, typename T, typename U>
    struct has_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() == std::declval<U>())>>
        : std::is_convertible<decltype(std::declval<T>() == std::declval<U>()), R>
    {};

    template<typename T, typename U>
    struct has_nothrow_equal<T, U, std::void_t<decltype(std::declval<T>() == std::declval<U>())>>
        : std::bool_constant<noexcept(std::declval<T>() == std::declval<U>())>
    {};

    template<typename R, typename T, typename U>
    struct has_nothrow_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() == std::declval<U>())>>
        : std::bool_constant<(noexcept(std::declval<T>() == std::declval<U>()) && std::is_convertible_v<decltype(std::declval<T>() == std::declval<U>()), R>)>
    {};

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U, typename = std::void_t<>>
    struct has_not_equal
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_not_equal_r
        : std::false_type
    {};

    template<typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_not_equal
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_not_equal_r
        : std::false_type
    {};

    template<typename T, typename U>
    struct has_not_equal<T, U, std::void_t<decltype(std::declval<T>() != std::declval<U>())>>
        : std::true_type
    {};

    template<typename R, typename T, typename U>
    struct has_not_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() != std::declval<U>())>>
        : std::is_convertible<decltype(std::declval<T>() != std::declval<U>()), R>
    {};

    template<typename T, typename U>
    struct has_nothrow_not_equal<T, U, std::void_t<decltype(std::declval<T>() != std::declval<U>())>>
        : std::bool_constant<noexcept(std::declval<T>() != std::declval<U>())>
    {};

    template<typename R, typename T, typename U>
    struct has_nothrow_not_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() != std::declval<U>())>>
        : std::bool_constant<(noexcept(std::declval<T>() != std::declval<U>()) && std::is_convertible_v<decltype(std::declval<T>() != std::declval<U>()), R>)>
    {};

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U, typename = std::void_t<>>
    struct has_less
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_less_r
        : std::false_type
    {};

    template<typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_less
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_less_r
        : std::false_type
    {};

    template<typename T, typename U>
    struct has_less<T, U, std::void_t<decltype(std::declval<T>() < std::declval<U>())>>
        : std::true_type
    {};

    template<typename R, typename T, typename U>
    struct has_less_r<R, T, U, std::void_t<decltype(std::declval<T>() < std::declval<U>())>>
        : std::is_convertible<decltype(std::declval<T>() < std::declval<U>()), R>
    {};

    template<typename T, typename U>
    struct has_nothrow_less<T, U, std::void_t<decltype(std::declval<T>() < std::declval<U>())>>
        : std::bool_constant<noexcept(std::declval<T>() < std::declval<U>())>
    {};

    template<typename R, typename T, typename U>
    struct has_nothrow_less_r<R, T, U, std::void_t<decltype(std::declval<T>() < std::declval<U>())>>
        : std::bool_constant<(noexcept(std::declval<T>() < std::declval<U>()) && std::is_convertible_v<decltype(std::declval<T>() < std::declval<U>()), R>)>
    {};    

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U, typename = std::void_t<>>
    struct has_greater
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_greater_r
        : std::false_type
    {};

    template<typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_greater
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_greater_r
        : std::false_type
    {};

    template<typename T, typename U>
    struct has_greater<T, U, std::void_t<decltype(std::declval<T>() > std::declval<U>())>>
        : std::true_type
    {};

    template<typename R, typename T, typename U>
    struct has_greater_r<R, T, U, std::void_t<decltype(std::declval<T>() > std::declval<U>())>>
        : std::is_convertible<decltype(std::declval<T>() > std::declval<U>()), R>
    {};

    template<typename T, typename U>
    struct has_nothrow_greater<T, U, std::void_t<decltype(std::declval<T>() > std::declval<U>())>>
        : std::bool_constant<noexcept(std::declval<T>() > std::declval<U>())>
    {};

    template<typename R, typename T, typename U>
    struct has_nothrow_greater_r<R, T, U, std::void_t<decltype(std::declval<T>() > std::declval<U>())>>
        : std::bool_constant<(noexcept(std::declval<T>() > std::declval<U>()) && std::is_convertible_v<decltype(std::declval<T>() > std::declval<U>()), R>)>
    {};

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U, typename = std::void_t<>>
    struct has_less_equal
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_less_equal_r
        : std::false_type
    {};

    template<typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_less_equal
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_less_equal_r
        : std::false_type
    {};    

    template<typename T, typename U>
    struct has_less_equal<T, U, std::void_t<decltype(std::declval<T>() <= std::declval<U>())>>
        : std::true_type
    {};

    template<typename R, typename T, typename U>
    struct has_less_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() <= std::declval<U>())>>
        : std::is_convertible<decltype(std::declval<T>() <= std::declval<U>()), R>
    {};

    template<typename T, typename U>
    struct has_nothrow_less_equal<T, U, std::void_t<decltype(std::declval<T>() <= std::declval<U>())>>
        : std::bool_constant<noexcept(std::declval<T>() <= std::declval<U>())>
    {};

    template<typename R, typename T, typename U>
    struct has_nothrow_less_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() <= std::declval<U>())>>
        : std::bool_constant<(noexcept(std::declval<T>() <= std::declval<U>()) && std::is_convertible_v<decltype(std::declval<T>() <= std::declval<U>()), R>)>
    {};    

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U, typename = std::void_t<>>
    struct has_greater_equal
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_greater_equal_r
        : std::false_type
    {};

    template<typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_greater_equal
        : std::false_type
    {};

    template<typename R, typename T, typename U, typename = std::void_t<>>
    struct has_nothrow_greater_equal_r
        : std::false_type
    {};    

    template<typename T, typename U>
    struct has_greater_equal<T, U, std::void_t<decltype(std::declval<T>() >= std::declval<U>())>>
        : std::true_type
    {};

    template<typename R, typename T, typename U>
    struct has_greater_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() >= std::declval<U>())>>
        : std::is_convertible<decltype(std::declval<T>() >= std::declval<U>()), R>
    {};

    template<typename T, typename U>
    struct has_nothrow_greater_equal<T, U, std::void_t<decltype(std::declval<T>() >= std::declval<U>())>>
        : std::bool_constant<noexcept(std::declval<T>() >= std::declval<U>())>
    {};

    template<typename R, typename T, typename U>
    struct has_nothrow_greater_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() >= std::declval<U>())>>
        : std::bool_constant<(noexcept(std::declval<T>() >= std::declval<U>()) && std::is_convertible_v<decltype(std::declval<T>() >= std::declval<U>()), R>)>
    {};

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

These are the helper variable templates:

    template<typename T, typename U>
    inline constexpr auto has_equal_v = has_equal<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_equal_r_v = has_equal_r<R, T, U>::value;

    template<typename T, typename U>
    inline constexpr auto has_nothrow_equal_v = has_nothrow_equal<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_nothrow_equal_r_v = has_nothrow_equal_r<R, T, U>::value;

   // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U>
    inline constexpr auto has_not_equal_v = has_not_equal<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_not_equal_r_v = has_not_equal_r<R, T, U>::value;

    template<typename T, typename U>
    inline constexpr auto has_nothrow_not_equal_v = has_nothrow_not_equal<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_nothrow_not_equal_r_v = has_nothrow_not_equal_r<R, T, U>::value;

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U>
    inline constexpr auto has_less_v = has_less<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_less_r_v = has_less_r<R, T, U>::value;

    template<typename T, typename U>
    inline constexpr auto has_nothrow_less_v = has_nothrow_less<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_nothrow_less_r_v = has_nothrow_less_r<R, T, U>::value;

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U>
    inline constexpr auto has_greater_v = has_greater<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_greater_r_v = has_greater_r<R, T, U>::value;

    template<typename T, typename U>
    inline constexpr auto has_nothrow_greater_v = has_nothrow_greater<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_nothrow_greater_r_v = has_nothrow_greater_r<R, T, U>::value;

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U>
    inline constexpr auto has_less_equal_v = has_less_equal<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_less_equal_r_v = has_less_equal_r<R, T, U>::value;

    template<typename T, typename U>
    inline constexpr auto has_nothrow_less_equal_v = has_nothrow_less_equal<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_nothrow_less_equal_r_v = has_nothrow_less_equal_r<R, T, U>::value;

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

    template<typename T, typename U>
    inline constexpr auto has_greater_equal_v = has_greater_equal<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_greater_equal_r_v = has_greater_equal_r<R, T, U>::value;

    template<typename T, typename U>
    inline constexpr auto has_nothrow_greater_equal_v = has_nothrow_greater_equal<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto has_nothrow_greater_equal_r_v = has_nothrow_greater_equal_r<R, T, U>::value;    

    // -------------------------------------------------------------------------------------------------------------------------------------------------------

And this is the actual is_comparable type trait:

    template<typename T, typename U>
    struct is_comparable
        : std::bool_constant<
              std::conjunction_v<
                  has_equal<T, U>,
                  has_not_equal<T, U>,
                  has_less<T, U>,
                  has_greater<T, U>,
                  has_less_equal<T, U>,
                  has_greater_equal<T, U>
              > // std::conjunction_v
          > // std::bool_constant
    {}; // is_comparable

    template<typename R, typename T, typename U>
    struct is_comparable_r
        : std::bool_constant<
              std::conjunction_v<
                  has_equal_r<R, T, U>,
                  has_not_equal_r<R, T, U>,
                  has_less_r<R, T, U>,
                  has_greater_r<R, T, U>,
                  has_less_equal_r<R, T, U>,
                  has_greater_equal_r<R, T, U>
              > // std::conjunction_v
          > // std::bool_constant
    {}; // is_comparable_r

    template<typename T, typename U>
    struct is_nothrow_comparable
        : std::bool_constant<
              std::conjunction_v<
                  has_nothrow_equal<T, U>,
                  has_nothrow_not_equal<T, U>,
                  has_nothrow_less<T, U>,
                  has_nothrow_greater<T, U>,
                  has_nothrow_less_equal<T, U>,
                  has_nothrow_greater_equal<T, U>
              > // std::conjunction_v
          > // std::bool_constant
    {}; // is_nothrow_comparable

    template<typename R, typename T, typename U>
    struct is_nothrow_comparable_r
        : std::bool_constant<
              std::conjunction_v<
                  has_nothrow_equal_r<R, T, U>,
                  has_nothrow_not_equal_r<R, T, U>,
                  has_nothrow_less_r<R, T, U>,
                  has_nothrow_greater_r<R, T, U>,
                  has_nothrow_less_equal_r<R, T, U>,
                  has_nothrow_greater_equal_r<R, T, U>
              > // std::conjunction_v
          > // std::bool_constant
    {}; // is_nothrow_comparable_r

    template<typename T, typename U>
    inline constexpr auto is_comparable_v = is_comparable<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto is_comparable_r_v = is_comparable_r<R, T, U>::value;

    template<typename T, typename U>
    inline constexpr auto is_nothrow_comparable_v = is_nothrow_comparable<T, U>::value;

    template<typename R, typename T, typename U>
    inline constexpr auto is_nothrow_comparable_r_v = is_nothrow_comparable_r<R, T, U>::value;    

} // namespace meta
\$\endgroup\$
2
\$\begingroup\$

All looks plausible to me. The SFINAE is appropriately concise.


Stylistically, this is a case where I'd prefer a small number of massively long lines over a large number of shorter lines. Each of your cases takes four lines of varying length, which makes it hard to eyeball quickly and hard to check for places you might have made a cut-and-paste error.


template<typename R, typename T, typename U>
    struct has_nothrow_greater_equal_r<R, T, U, std::void_t<decltype(std::declval<T>() >= std::declval<U>())>>
        : std::bool_constant<(noexcept(std::declval<T>() >= std::declval<U>()) && std::is_convertible_v<decltype(std::declval<T>() >= std::declval<U>()), R>)>
    {};

Here you're checking that T >= U is convertible to R, and that T >= U is nothrow, but you're never checking whether static_cast<R>(T >= U) is nothrow. Is that intentional, or a bug, or doesn't-really-matter?


template<typename T, typename U>
struct is_nothrow_comparable
    : std::bool_constant<
          std::conjunction_v<
              has_nothrow_equal<T, U>,
              has_nothrow_not_equal<T, U>,
              has_nothrow_less<T, U>,
              has_nothrow_greater<T, U>,
              has_nothrow_less_equal<T, U>,
              has_nothrow_greater_equal<T, U>
          > // std::conjunction_v
      > // std::bool_constant
{}; // is_nothrow_comparable

template<typename T, typename U>
inline constexpr auto is_comparable_v = is_comparable<T, U>::value;

I would be inclined to write this the other way around, to avoid std::conjunction:

template<typename T, typename U>
inline constexpr bool is_comparable_v = 
    has_nothrow_equal_v<T, U> && has_nothrow_not_equal_v<T, U> &&
    has_nothrow_less_v<T, U> && has_nothrow_less_equal_v<T, U> &&
    has_nothrow_greater_v<T, U> && has_nothrow_greater_equal_v<T, U>;

template<typename T, typename U>
struct is_comparable : std::bool_constant<is_comparable_v<T, U>> {};

But I admit most of the difference in style there is in the whitespace. :)


typename = std::void_t<> is a needlessly verbose way of writing typename = void.


The fact that has_equal takes three template parameters (one defaulting to void) makes me vaguely uneasy. When the standard library does type traits, it always makes sure that the standard entry points take exactly the right number of parameters, with no defaults:

template<class T, class U, class> struct has_equal_impl                                                                 : std::false_type {};
template<class T, class U>        struct has_equal_impl<T, U, decltype(std::declval<T>() == std::declval<U>(), void())> : std::true_type {};

template<class T, class U> struct has_equal : has_equal_impl<T, U, void> {};

I don't know whether there's a specific use-case this is good for, or whether it's library-vendor paranoia, or whether it's completely unnecessary. (Leave a comment if you know!)


EDIT: In the comments, I realized that

static_assert(has_equal_r<std::string, std::string, bool>::value);

fails, because the template expects its parameters in the unusual order "destination-type, operands" instead of the usual "operands, destination-type" (cf. is_convertible, Concepts Lite syntax, auto function declaration syntax). I strongly recommend reversing the order of the operands.

Plus, if you do that, you can collapse the _r versions into the other ones: has_equal<T, U, R=bool>. "Has an == convertible to bool" is not quite the same notion semantically as "Has an =="; but the difference only shows up in fully generic contexts. Another option (requiring even more metaprogramming, sadly) is to special-case has_equal<T, U, void> as meaning "has == of any type". (I say "requires more metaprogramming" because for some reason is_convertible<R, void> is usually false.)


Another late-breaking comment that I forgot to write down last time: The use of the suffix _r disturbs me. I'm familiar with _t and _v (and I've had occasion to use _f on MSVC, which supports constexpr inline functions but not constexpr inline variables), but _r confused me until I saw that you were using it to mean literally "this template takes an additional parameter named _r". That's not a good reason to use a single-character suffix like that.

glibc also uses _r to mean "reentrant-safe", as in qsort_r and strtok_r.

If you collapse the _r versions into the two-argument versions as I suggest in the previous point, then you won't need to worry about naming the _r versions, so you solve two problems at once. :)

\$\endgroup\$
  • \$\begingroup\$ Thanks for pointing out the missing static_cast. The issue is, static_cast gives me a compiletime error instead of a std::bool_constant<false> if the type is not convertible. Maybe there is a workaround, I'll look into that later \$\endgroup\$ – Yamahari Jun 19 '17 at 10:45
  • \$\begingroup\$ Works fine for me; I suspect you have a typo or cut-and-paste error somewhere. By the way, I didn't notice until after writing my test cases that you take the arguments in the order <R,T,U> instead of my expected <T,U,R>; I'll edit my answer to include that. \$\endgroup\$ – Quuxplusone Jun 19 '17 at 16:09
  • \$\begingroup\$ Yea I had some thinking errors, the static_castworks fine. :) In reference to your edit: std::is_invocable. This is why also put the template parameters in the order they are right now. \$\endgroup\$ – Yamahari Jun 20 '17 at 8:30
  • \$\begingroup\$ Re std::is_invocable: Thanks, I was unaware of that _r! But just apply my argument upstream to the C++ committee: that _r is also dumb. What they should have done was add a std::invoke_result_t<F, Args...> that gives back the actual result type, and then std::is_convertible_v<that, R> would give you the answer you're looking for. (Oh wait. They did do that. So this is_invocable_r_v thing is 100% superfluous confusion that should have been left out of the standard.) \$\endgroup\$ – Quuxplusone Jun 21 '17 at 22:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.