Home > Mobile >  Changing position of C 20 "requires clause" gives different results
Changing position of C 20 "requires clause" gives different results

Time:11-13

Consider the following type MyType together with a SFINAE-style detection whether some type U is MyType or not:

template <class T>
struct MyType {
    using value_type = T;
    static constexpr bool is_my_type = true;
    value_type value{};
};


// A constexpr function returning true if <U> is NOT "MyType".
template <class U>
constexpr std::enable_if_t<U::is_my_type, bool> IsNotMyType(U const *) {
    return false;
}

template <class>
constexpr bool IsNotMyType(...) {
    return true;
}

Then I define an equality operator (which is necessary for reproduction) and two spaceship operators, one for a comparison of MyType<T> with MyType<U>, and one for a comparison of MyType<T> with some type U which is not a MyType:

// Necessary for reproduction
template <class T, class U>
bool operator==(MyType<T> const & lhs, MyType<U> const & rhs) {
    return lhs.value == rhs.value;
}


template <class T, class U>
std::compare_three_way_result_t<
    typename MyType<T>::value_type,
    typename MyType<U>::value_type> 
  operator<=>(MyType<T> const & lhs, MyType<U> const & rhs)
  requires(
    std::three_way_comparable_with<typename MyType<T>::value_type, typename MyType<U>::value_type>)
{
    return lhs.value <=> rhs.value;
}


template <class T, class U>
std::compare_three_way_result_t<typename MyType<T>::value_type, U> 
  operator<=>(MyType<T> const & lhs, U const & rhs)
  // Problematic requires clause
  requires(
    IsNotMyType<U>(nullptr) &&
    std::three_way_comparable_with<typename MyType<T>::value_type, U>)
{
    return lhs.value <=> rhs;
}

Usage:

int main(){
    MyType<int> m;
    [[maybe_unused]] auto t = m <=> m;
}

Live example on godbolt. gcc and MSVC compile this without issue with C 20, but Clang runs into a "fatal error: recursive template instantiation exceeded maximum depth" (both with libstdc and libc ):

test.cc:41:6: fatal error: recursive template instantiation exceeded maximum depth of 1024
std::compare_three_way_result_t<typename MyType<T>::value_type, U>
     ^
/usr/lib/gcc/x86_64-linux-gnu/10/../../../../include/c  /10/concepts:306:10: note: while substituting deduced template arguments into function template 'operator<=>' [with T = int, U = remove_reference_t<MyType<int>>]
          { __t <  __u } -> __boolean_testable;
                ^
/usr/lib/gcc/x86_64-linux-gnu/10/../../../../include/c  /10/concepts:306:6: note: in instantiation of requirement here
          { __t <  __u } -> __boolean_testable;
            ^~~~~~~~~~
/usr/lib/gcc/x86_64-linux-gnu/10/../../../../include/c  /10/concepts:304:4: note: while substituting template arguments into constraint expression here
        = requires(const remove_reference_t<_Tp>& __t,
          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/lib/gcc/x86_64-linux-gnu/10/../../../../include/c  /10/compare:420:10: note: while checking the satisfaction of concept '__partially_ordered_with<MyType<int>, MyType<int>>' requested here
      && __detail::__partially_ordered_with<_Tp, _Tp>
         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/lib/gcc/x86_64-linux-gnu/10/../../../../include/c  /10/compare:420:10: note: while substituting template arguments into constraint expression here
      && __detail::__partially_ordered_with<_Tp, _Tp>
         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/lib/gcc/x86_64-linux-gnu/10/../../../../include/c  /10/compare:430:10: note: (skipping 2039 contexts in backtrace; use -ftemplate-backtrace-limit=0 to see all)
      && three_way_comparable<_Up, _Cat>
         ^
/usr/lib/gcc/x86_64-linux-gnu/10/../../../../include/c  /10/compare:420:10: note: while substituting template arguments into constraint expression here
      && __detail::__partially_ordered_with<_Tp, _Tp>
         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/lib/gcc/x86_64-linux-gnu/10/../../../../include/c  /10/compare:430:10: note: while checking the satisfaction of concept 'three_way_comparable<MyType<int>, std::partial_ordering>' requested here
      && three_way_comparable<_Up, _Cat>
         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/lib/gcc/x86_64-linux-gnu/10/../../../../include/c  /10/compare:430:10: note: while substituting template arguments into constraint expression here
      && three_way_comparable<_Up, _Cat>
         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
test.cc:46:5: note: while checking the satisfaction of concept 'three_way_comparable_with<int, MyType<int>, std::partial_ordering>' requested here
    std::three_way_comparable_with<typename MyType<T>::value_type, U>)
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
test.cc:54:33: note: while substituting deduced template arguments into function template 'operator<=>' [with T = int, U = MyType<int>]
    [[maybe_unused ]]auto t = m <=> m;

If I move the trailing requires clause of the second operator<=> to before the return value, Clang is happy (full example on godbolt):

template <class T, class U>
  // Problematic requires clause
  requires(
    IsNotMyType<U>(nullptr) &&
    std::three_way_comparable_with<typename MyType<T>::value_type, U>)
std::compare_three_way_result_t<typename MyType<T>::value_type, U> 
  operator<=>(MyType<T> const & lhs, U const & rhs)
{
    return lhs.value <=> rhs;
}

From what I understood so far, e.g. from this question, the position of the requires clause should not matter, but apparently Clang thinks differently. Who is right? Are there actually cases according to the standard where a different position of the requires clause can cause functional differences?

CodePudding user response:

test.cc:41:6: fatal error: recursive template instantiation exceeded maximum depth of 1024

Regardless of whether this is legal C or not (and it is), this kind of error represents either a compiler bug or a compiler limitation. This is doubly obvious when you see how easy it is to fix:

#include <compare>

template <class T>
struct MyType {
    using value_type = T;
    static constexpr bool is_my_type = true;
    value_type value{};
};

template<typename T>
concept IsMyType = T::is_my_type;


// Necessary for reproduction
template <class T, class U>
bool operator==(MyType<T> const & lhs, MyType<U> const & rhs) {
    return lhs.value == rhs.value;
}


template <class T, class U>
  std::compare_three_way_result_t<
    typename MyType<T>::value_type,
    typename MyType<U>::value_type> operator<=>(MyType<T> const & lhs, MyType<U> const & rhs)
  requires(
    std::three_way_comparable_with<typename MyType<T>::value_type, typename MyType<U>::value_type>)

{
    return lhs.value <=> rhs.value;
}


template <class T, class U>
std::compare_three_way_result_t<typename MyType<T>::value_type, U> 
  operator<=>(MyType<T> const & lhs, U const & rhs)
  // Problematic requires clause
  requires(
    !IsMyType<U> &&
    std::three_way_comparable_with<typename MyType<T>::value_type, U>)
{
    return lhs.value <=> rhs;
}

Take your constexpr function and turn it into a concept, and voila: it's fixed. It's also better code, because seriously: stop using old bad SFINAE in C 20.

  • Related