Recursive concept/type_traits on tuple-like types

Viewed 127

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?
2 Answers
  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

I wrote a GENERIC version (using SFINAE) with customizable predicate that will work with all the specialized tuple-like types (those that have specializations for std::tuple_element and std::tuple_size).
https://godbolt.org/z/Yf889GY6E


#include <cstddef>
#include <type_traits>
#include <tuple>

template <typename, typename = void>
struct is_complete : std::false_type {};

template <typename T>
struct is_complete<T, std::void_t<decltype(sizeof(T))>> : std::true_type {};

// catching all standard (and specialized) tuple-like types
template <typename T>
struct is_tuple_like : is_complete<std::tuple_size<T>> {};

template <typename T, bool = is_tuple_like<T>::value>
struct get_tuple_size
{
    constexpr static size_t value = 0;
};

template <typename T>
struct get_tuple_size<T, true>
{
    constexpr static size_t value = std::tuple_size_v<T>;
};

// generic solution with predicate
template <template <typename> class PredicateT,
    typename T, 
    bool = is_tuple_like<T>::value, 
    typename = decltype(std::make_index_sequence<get_tuple_size<T>::value>{})>
struct tuple_conjunction : PredicateT<T> {};

template <template <typename> class PredicateT,
    typename T, 
    size_t ... I>
struct tuple_conjunction<PredicateT, T, true, std::index_sequence<I...>> : 
    std::conjunction<tuple_conjunction<PredicateT, std::tuple_element_t<I, T>>...> {}; 


////////////////////////////

template <typename T, typename = void>
struct has_meow : std::false_type {};

template <typename T>
struct has_meow<T, std::void_t<decltype(std::declval<T>().mew())>> : std::true_type {};

template <typename T>
using meowable_pred = std::disjunction<std::is_integral<T>, has_meow<T>>;

template <typename T>
using meowable = tuple_conjunction<meowable_pred, T>;


#include <array>

struct cat
{
    void mew() {}
};

struct dummy{};

int main()
{
    static_assert(!is_complete<std::tuple_element<0, cat>>::value);
    static_assert(is_complete<std::tuple_element<0, std::tuple<cat>>>::value);

    static_assert(meowable<int>::value);
    static_assert(meowable<cat>::value);
    static_assert(!meowable<dummy>::value);

    static_assert(meowable<std::tuple<long>>::value);

    static_assert(meowable<std::tuple<int, long>>::value);
    static_assert(meowable<std::tuple<cat, long>>::value);
    static_assert(meowable<std::tuple<int, std::tuple<cat, long>>>::value);
    
    static_assert(!meowable<std::tuple<int, std::tuple<cat, long>, dummy>>::value);

    // std::array 
    static_assert(meowable<std::array<cat, 42>>::value);
    static_assert(meowable<std::tuple<int, std::tuple<cat, long, std::tuple<std::array<cat, 42>>>>>::value);

    return 0;
};

With some adjustments it will also work with C++11.

  • You can provide any predicate you want for a recursive check. The example shows a usage with meowable_pred predicate.

OLD ANSWER

Here is a simple recursive SFINAE solution for C++17 (and 11 for some adjustments):
https://godbolt.org/z/cezqhb99b


#include <type_traits>

template <typename T, typename = void>
struct has_meow : std::false_type {};

// this is a customization point to deduce tuple-like traits
// you can define some default logic here,
// but for the sake of example let it be false by default
template <typename>
struct is_tuple_like : std::false_type {};

template <typename T>
struct has_meow<T, std::void_t<decltype(std::declval<T>().mew())>> : std::true_type {};

template <typename T>
struct meowable : std::disjunction<std::is_integral<T>, has_meow<T>> {};

template <template <typename...> class TupleT, typename ... T>
struct meowable<TupleT<T...>> : std::conjunction<
                                          is_tuple_like<TupleT<T...>>,
                                          std::conjunction<meowable<T>...
                                       > {};

#include <tuple>
#include <array>

// this will also catch std::pair
template <typename ... T>
struct is_tuple_like<std::tuple<T...>> : std::true_type {};

template <typename T, size_t N>
struct is_tuple_like<std::array<T, N>> : std::true_type {};

struct cat
{
    void mew() {}
};

int main()
{
    static_assert(meowable<int>::value);
    static_assert(meowable<std::tuple<int, long>>::value);
    static_assert(meowable<cat>::value);
    static_assert(meowable<std::tuple<cat, long>>::value);
    static_assert(meowable<std::tuple<int, std::tuple<cat, long>>>::value);

    return 0;
};
Related