对类元组类型递归 concept/type_traits

Recursive concept/type_traits on tuple-like types

说我正在尝试实施一个概念 meowable

  1. 整数类型是可喵的。
  2. Class 成员函数为 meow 的类型是可喵的。这是最终目标,但当前问题没有关注它。
  3. 只有可喵元素的类似元组的类型是可喵的。
  4. std::ranges::range with meowable 元素是可喵喵的。这是最终目标,但当前问题没有关注它。

然后我想出了这个实现(尽可能简化):

#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);

但是有些编译器不喜欢它(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.

但是,如果我取消注释倒数第二行代码,所有编译器都会很高兴。 (概念的实例化点?)

所以我的问题是:

  1. 为什么会出现这种行为? (编译器错误或类似“格式错误的 NDR”之类的东西?)
  2. 我怎样才能实现我的目标?
  1. Why this behavior? (compiler bug or something like "ill-formed NDR"?)

这显然是 GCC-trunk 和 Clang-trunk 的错误,这里的问题是 GCC/Clang 没有根据 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?

根据归约结果用模板函数替换lambda

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

我写了一个 GENERIC 版本(使用 SFINAE),带有 可自定义的谓词 ,它将适用于所有专门的 tuple-like 类型(具有 std::tuple_elementstd::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;
};

经过一些调整,它也适用于 C++11。

  • 您可以为递归检查提供您想要的任何谓词。该示例显示了 meowable_pred 谓词的用法。

旧答案

这是一个简单的递归 SFINAE 解决方案,适用于 C++17(和 11 进行一些调整):
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;
};