Search code examples
c++c++20c++-concepts

Recursive concept/type_traits on tuple-like types


Say I was trying to implement a concept meowable that

  1. Integral types are meowable.
  2. Class types with member function meow are meowable. This is in the final target but the current question doesn't focus on it.
  3. Tuple-like types with only meowable elements are meowable.
  4. std::ranges::range with meowable elements are meowable. This is in the final target but the current question doesn't focus on it.

Then I came up with this implementation(simplified as I could):

#include <concepts>
#include <type_traits>
#include <ranges>
#include <utility>
#include <tuple>

template<class T>
concept meowable_builtin = std::integral<T>;

template<class T, std::size_t I>
concept has_tuple_element = requires (T t) {
  typename std::tuple_element<I, T>::type;
  { get<I>(t) } -> std::convertible_to<std::tuple_element_t<I, T>&>;
};

template<class T>
concept tuple_like = requires {
    typename std::tuple_size<T>::type;
    { std::tuple_size_v<T> } -> std::convertible_to<std::size_t>;
  } &&
  []<std::size_t...I>(std::index_sequence<I...>) {
    return (has_tuple_element<T, I> && ...);
  } (std::make_index_sequence<std::tuple_size_v<T>>{});

template<class               T> struct is_meowable:    std::false_type{};
template<meowable_builtin    T> struct is_meowable<T>: std::true_type{};

template<tuple_like T>
struct is_meowable<T>
  : std::bool_constant<
      []<std::size_t...I>(std::index_sequence<I...>) {
        return (is_meowable<std::tuple_element_t<I, T>>::value && ...);
      } (std::make_index_sequence<std::tuple_size_v<T>>{})
    > {};

template<class T>
concept meowable_tuple = tuple_like<T> && is_meowable<T>::value;

template<class T>
concept meowable = is_meowable<T>::value;

static_assert(meowable<int>);
//static_assert(tuple_like<std::tuple<int>>);
static_assert(is_meowable<std::tuple<int>>::value);

But some compilers don't like it (https://godbolt.org/z/5vMTEhTdq):

1. GCC-12 and above:   internal compiler error.
2. GCC-11:             accepted.
3. Clang-13 and above: static_assert fired.
4. MSVC-v19:           accepted.

However, if I uncomment the second last line of code, all compilers are happy. (Instantiation point of concepts?)

So my questions are:

  1. Why this behavior? (compiler bug or something like "ill-formed NDR"?)
  2. How can I achieve my target?

Solution

    1. Why this behavior? (compiler bug or something like "ill-formed NDR"?)

    This is apparently a bug of GCC-trunk and Clang-trunk, the issue here is that GCC/Clang doesn't properly handle the template partial specialization based on the concept initialized by the lambda. Reduced

    template<class>
    concept C = [] { return true; } ();
    
    template<class T> 
    struct S {};
    
    template<class T>
      requires C<T>
    struct S<T> { constexpr static bool value = true; };
    
    // static_assert(C<int>);
    static_assert(S<int>::value);
    
    1. How can I achieve my target?

    Replace lambda with the template function based on the reduced result

    template<class T, std::size_t...I>
    constexpr bool all_has_tuple_element(std::index_sequence<I...>) {
      return (has_tuple_element<T, I> && ...);
    }
    
    template<class T>
    concept tuple_like = requires {
      typename std::tuple_size<T>::type;
      { std::tuple_size_v<T> } -> std::convertible_to<std::size_t>;
    } && all_has_tuple_element<T>(std::make_index_sequence<std::tuple_size_v<T>>{});
    

    Demo