如何将模板参数包参数限制为“链”序列?

How can I constrain template parameter pack arguments to a "chain" sequence?

提问人:SKNB 提问时间:11/5/2023 最后编辑:Abderrahmene Rayene MihoubSKNB 更新时间:11/23/2023 访问量:2088

问:

假设我有两个类:

template <typename X, typename Y>
class Functor {};

template <typename Start, typename End, typename ...Functors>
class Template {};

Template具有以下限制:

  • 都必须是类型FunctorsFunctor

  • 所有这些都必须处于序列中,以便Functor

    • 第一个必须作为其第一个参数FunctorStart
    • 最后一个必须作为其第二个参数FunctorEnd
    • 每个 的第一个参数是前一个参数的第二个参数FunctorFunctor

    例如 等。Functor<A,B>, Functor<B, C>, Functor<C, D>, ...

例:

开始于:char

结尾为:long

Template<char, long, Functor<char, A>, Functor<A, B>, Functor<B, C>, Functor<C, long>> t;

                1         2         3         4
           ├─────────┼─────────┼─────────┼─────────┤
argument: char       A         B         C        long
Functor #
  = 1      Functor<char, A>,
    2                Functor<A, B>,
    3                           Functor<B, C>,
    4                                    Functor<C, long>

法典

namespace ns
{
    template <typename X, typename Y = X>
    class Functor
    {
    public:
        using first  = X;
        using second = Y;
        Functor(X lVal) : x(lVal) {}
    private:
        X x;
    };

    template <typename Start, typename End, typename ...Functors>
        requires(std::is_convertible_v<Functors, Functor> && ...)    //error
    class Template
    {
        // How does one use `std::is_convertible_v` on
        // an un-specialized template class?
    };

    template <typename Start, typename End>
    class Template<Start, End, Functor<Start, End>>
    {};
}

问题:

  1. 最好的方法是什么?
    • 这可以通过折叠表达式来完成吗?
    • 还是概念?
  2. 如何在非专用模板类上使用(或任何其他元编程特征)?std::is_convertible
C ++20 模板元编程 C++概念

评论


答:

4赞 Sam Varshavchik 11/5/2023 #1

这回答了主要问题。以下方法似乎有效。这样就可以对 s 的正确序列进行基本检查,并将其转化为概念,或者添加类型特征样式包装器等,可以作为家庭作业:Functorusing

class Start;
class End;

template <typename X, typename Y> class Functor {};

template<typename required, typename NextFunctor,
     typename ...RemainingFunctors> struct FunctorValidate;

template<typename required>
struct FunctorValidate<required, Functor<required, End>> {

    typedef void type_t;
};

template<typename required, typename NextFunctorType,
    typename FirstRemainingFunctor, typename ...RemainingFunctors>
struct FunctorValidate<required, Functor<required, NextFunctorType>,
    FirstRemainingFunctor, RemainingFunctors...>
    : FunctorValidate<NextFunctorType, FirstRemainingFunctor,
              RemainingFunctors...>
{
};

template<typename ...AllFunctors> struct FunctorChain
    : FunctorValidate<Start, AllFunctors...>
{
};

class A;
class B;
class C;

typedef FunctorChain<Functor<Start, A>,
             Functor<A, B>,
             Functor<B, C>,
             Functor<C, End>>::type_t ok1;

typedef FunctorChain<Functor<Start, End>>::type_t ok2;

#if 0
typedef FunctorChain<Functor<A, B>,
             Functor<B, C>,
             Functor<C, End>>::type_t error1;

typedef FunctorChain<Functor<Start, A>,
             Functor<A, B>,
             Functor<B, C>>::type_t error2;

typedef FunctorChain<Functor<Start, A>,
             Functor<B, C>,
             Functor<C, End>>::type_t error3;

#endif
10赞 Ted Lyngmo 11/5/2023 #2

你可以先添加一个类型特征来检查模板参数是否属于类型:Functor

template <typename X, typename Y>
class Functor {
   public:
    using first_type = X;
    using second_type = Y;
};

// type trait to check if a type is a Functor
template <class...>
struct is_Functor : std::false_type {};

template <class X, class Y>
struct is_Functor<Functor<X, Y>> : std::true_type {};

template <class T>
inline constexpr bool is_Functor_v = is_Functor<T>::value;

然后使用带有折叠表达式的子句要求它们实际上都是类型的:Functorrequires

// Require Functors
template <typename Start, typename End, typename... Functors>
    requires(is_Functor_v<Functors> && ...)
class Template {

然后断言第一个参数是第一个参数,最后一个参数是第二个参数:FunctorStartFunctorEnd

    // helper type to get a Functor (type) at a specific index:
    template <std::size_t I>
    using funcat = typename std::tuple_element_t<I, std::tuple<Functors...>>;

    static_assert(std::is_same_v<
                  Start, typename funcat<0>::first_type>);

    static_assert(std::is_same_v<
                  End, typename funcat<sizeof...(Functors) - 1>::second_type>);

然后使用 lambda 和 fold 表达式检查每个参数的第二个参数是否与下一个参数的第一个参数类型相同:FunctorFunctor

    static_assert([]<std::size_t... I>(std::index_sequence<I...>) {
        return (... && std::is_same_v<typename funcat<I>::second_type,
                                      typename funcat<I + 1>::first_type>);
    }(std::make_index_sequence<sizeof...(Functors) - 1>()));

如果您更喜欢此声明,也可以创建类型特征:conceptis_Functor

template <class T>
concept functor = is_Functor_v<T>;

template <typename Start, typename End, functor... Functors>
class Template {
   //
};

演示


至于第二个问题,“如何在非专用模板类上使用 std::is_convertible(或任何其他元编程特征)?”,您可以再次创建一个类型特征,或者检查是否可以由提供的类型实例化,而无需显式提供任何模板参数。conceptFunctor

例:concept

template <class F, template <class...> class T>
concept deducible = requires(F&& f) {
    T(std::forward<F>(f));
};

然后,类模板需要使用每个实际模板参数将转换为的类型:Functor

template <typename Start, typename End, deducible<Functor>... Functors>
class Template {
    template <std::size_t I>
    using funcat =
        std::tuple_element_t<I,
            std::tuple<decltype(Functor(std::declval<Functors>()))...>>;
    // converted to Functor     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^            

    static_assert(std::is_same_v<
                  Start, typename funcat<0>::first_type>,
                  "First Functor::first_type != Start");

    static_assert(std::is_same_v<
                  End, typename funcat<sizeof...(Functors) - 1>::second_type>,
                  "Last Functor::second_type != End");

    static_assert([]<std::size_t... I>(std::index_sequence<I...>) {
        return (... && std::is_same_v<typename funcat<I>::second_type,
                                      typename funcat<I + 1>::first_type>);
    }(std::make_index_sequence<sizeof...(Functors) - 1>()),
                  "Functor_n<..., T> != Functor_n+1<T, ...>");
};

演示,其中 a 可转换为 a(奇数转换,但这只是一个例子)。std::pair<X, Y>Functor<X, Y>

14赞 Jarod42 11/6/2023 #3

使用 (ab) 运算符重载时,您可以

// Used std::type_identity as wrapper
// Operator+, but no implementation needed
template <typename A, typename B, typename C>
std::type_identity<Functor<A, C>>
operator +(std::type_identity<Functor<A, B>>, std::type_identity<Functor<B, C>>);

然后只需检查操作是否“有效”。

template <typename Start, typename End, typename... Functors>
requires(std::is_same_v<std::type_identity<Functor<Start, End>>,
                        decltype((std::type_identity<Functors>{} + ...))>)
class Template {
    //...
};

演示

评论

1赞 aschepler 11/6/2023
我建议要么把它放在命名空间中,要么使用单独的包装器,而不是让它更不可能被意外使用或与其他一些类似的滥用行为发生冲突。operator+type_identity
4赞 user3840170 11/6/2023 #4

您可以使用模板专用化作为类型级模式匹配引擎来创建类模板,该模板不仅可以验证 s 链,还可以提取其“域”和“共域”类型。Functor

#include <type_traits>

template <typename T, typename U>
struct Functor { /* […] */ };

template <typename ...Fs>
struct chain_types;

template <typename T, typename U>
struct chain_types<Functor<T, U>>
{
    using from_t = T;
    using into_t = U;
};

template <typename T, typename U, typename ...Fs>
    requires (std::is_same_v<U, typename chain_types<Fs...>::from_t>)
struct chain_types<Functor<T, U>, Fs...>
{
    using from_t = T;
    using into_t = chain_types<Fs...>::into_t;
};

template <typename T, typename U, typename ...Fs>
    requires (
        std::is_same_v<T, typename chain_types<Fs...>::from_t> &&
        std::is_same_v<U, typename chain_types<Fs...>::into_t>
    )
class Template { /* […] */ };

测试用例:

class A;
class B;
class C;

static_assert(
    std::is_same_v<A, chain_types<Functor<A, B>, Functor<B, C>>::from_t>);
static_assert(
    std::is_same_v<C, chain_types<Functor<A, B>, Functor<B, C>>::into_t>);
static_assert(
    requires { typename Template<A, C, Functor<A, B>, Functor<B, C>>; });
5赞 ecatmur 11/7/2023 #5

作为单个 requires-expression:

    requires requires(Functors... f) {
        []<typename... X, typename... Y>(Functor<X, Y>&...)
            requires std::same_as<void(Start, Y...), void(X..., End)> {
        }(f...);
    }

首先,我们推导出每个的 和 ;如果其中任何一个不是 的实例化,则此步骤将失败。(严格来说,它还允许派生自 的类型; 为了防止这种情况,您可以使用 。然后,我们检查类型链是否与链相同(注意:不是!如果形成从 到 的链,这将恰恰成立。XYFunctorFunctorsFunctorFunctor<X, Y>std::type_identityStart, Y...X..., EndStart, X...<X, Y>...StartEnd

请注意,它也适用于 ,您已将其列为单独的情况,而 对于 ,即如果 和 是同一类型,它将允许链为空;如果要禁止这样做,可以添加为额外约束。<Start, End, Functor<Start, End>><Start, Start>StartEndsizeof...(Functors) != 0u

我正在将函数类型用于类型列表,这很简洁,但确实有衰减类型的潜在缺点;您同样可以使用 例如,这将允许放宽对 例如 ( 是可转换为 iff 每个是可转换为其各自的 )。std::tuplestd::convertible_tostd::tuple<T...>std::tuple<U...>TU

如果传递了无效链,gcc 将输出一个错误,结尾如下:

note: the expression 'is_same_v<_Tp, _Up> [with _Tp = void(char, A, C, C, int); _Up = void(char, A, B, C, long int)]' evaluated to 'false'

演示

编写约束的一种稍微冗长但可能更清晰的方法是:

    requires requires(std::tuple<Functors...> f) {
        []<typename... X, typename... Y>(std::tuple<Functor<X, Y>...>)
            requires requires(std::tuple<Start, Y...> c, std::tuple<X..., End> d) {
                []<typename... C, typename... D>(std::tuple<C...>, std::tuple<D...>)
                    requires (std::same_as<C, D> and...) {}(c, d);
            } {}(f); }

在这里,我们使用类型演绎来形成类型列表,然后在元素类型比较上执行连词折叠,在无效链上给出错误,如下所示:C := {Start, Y...}D := {X..., End}

note: the expression '(same_as<C, D> && ...) [with C = {char, A, C, C, int}; D = {char, A, B, C, long int}]' evaluated to 'false'

演示

评论

1赞 Ted Lyngmo 11/7/2023
std::same_as<void(Start, Y...), void(X..., End)>...是很长一段时间以来我见过的最好的东西之一。明!
2赞 SKNB 11/13/2023
同意,非常有创意!
0赞 user3840170 11/19/2023
不幸。std::same_as<void(int [69]), void(int [420])>
0赞 ecatmur 11/20/2023
@user3840170真的,但数组类型不能是返回类型,所以这里不是问题。
1赞 ecatmur 11/23/2023
@user3840170啊,我一定在想一个类似的问题——不过这个词有点暗示。Functor
0赞 Ryan 11/9/2023 #6

若要实现在 C++20 中查找的约束,可以使用概念和要求来约束模板参数。需要确保参数包中的每个类型都是 Functor 模板的实例化,并且它们形成一个链,如上所述。

下面是一个概念,用于检查类型是否是 Functor 的专用化:

#include <type_traits>

template <typename T>
concept IsFunctor = requires {
    typename T::first;
    typename T::second;
};

template <typename T, typename U>
concept SameAs = std::is_same_v<T, U>;

template <typename...>
struct are_chainable : std::false_type {};

template <typename T>
struct are_chainable<T> : std::true_type {}; // base case for a single element

template <typename T, typename U, typename... Rest>
requires(IsFunctor<T> && IsFunctor<U>)
struct are_chainable<T, U, Rest...>
    : std::conditional_t<SameAs<typename T::second, typename U::first>,
                         are_chainable<U, Rest...>,
                         std::false_type> {};

// Now define the Template class with the proper constraints
template <typename Start, typename End, typename... Functors>
requires(are_chainable<Functors...>::value && SameAs<typename Start::second, typename decltype((Functors{}, ...))::first>::value && SameAs<typename decltype((... , Functors{}))::second, End>::value)
class Template {};

are_chainable结构以递归方式检查 Functor 专用化序列是否可链接。

IsFunctor 概念检查是否存在第一成员类型和第二成员类型,这些类型是将类型视为函子所必需的。

SameAs 概念是一个帮助程序,用于使用 std::is_same_v 确保两种类型相同。

Template 声明中的 requires 子句使用 are_chainable 来确保 Functors 包形成从开始到结束的有效链。fold 表达式 (Functors{}, ...) 用于从包中的最后一个 Functor 中获取第一个类型,从包中的第一个 Functor 中获取最后一个类型,确保它们分别与 Start 和 End 模板参数匹配。

fold 表达式 (Functors{}, ...) 创建一个逗号分隔的包扩展列表,当在 decltype 的上下文中使用时,将生成包中最后一个元素的类型。这是从参数包中“提取”最后一个元素类型的技巧。

但是请注意,由于在示例中应该是值时使用了 Start 和 End 作为类型,因此上述代码无法按原样编译。Start 和 End 需要分别对应于第一个 Functor 的第一个 Functor 和最后一个 Functor 的第二个 Functor 的类型。下面是如何声明此类模板的示例:

// Example usage:

// Define some type aliases for demonstration
using A = int;
using B = double;
using C = float;

// Declare functors
using FunctorAB = Functor<A, B>;
using FunctorBC = Functor<B, C>;
using FunctorCD = Functor<C, long>; // C to long as per your chain requirement

// Declare template chain
Template<A, long, FunctorAB, FunctorBC, FunctorCD> valid_chain;

在上面的 Template 声明中,不再需要 Start 和 End 来派生 Functor,它们只是普通类型。相反,约束确保函子包中的类型是连续的,因为 1 的第二种类型是下一个的第一种类型,第一个函子的第一种类型是 Start,最后一个函子的第二种类型是 End。