使用模板模板函数获取模板参数的数量。

4

我不确定这是否可能,但我希望能够计算任何类的模板参数数量,例如:

template <typename T>
class MyTemplateClass { ... };

template <typename T, typename U>
class MyTemplateClass2 { ... };

要求满足template_size<MyTemplateClass>() == 1template_size<MyTemplateClass2>() == 2。我是一个刚开始接触模板的初学者,所以我写了这个函数,但它当然不起作用:

template <template <typename... Ts> class T>
constexpr size_t template_size() {
     return sizeof...(Ts);
}

因为无法引用Ts。我也知道在处理变参模板时可能会遇到问题,但这不是我的应用程序所遇到的情况。
提前感谢。

你打算使用某些类型来实例化这个模板吗?因为通常情况下,一个模板接受的参数数量是一个图灵完备问题,只能通过尝试传递参数来确定。 - Yakk - Adam Nevraumont
3个回答

2
#include <utility>
#include <iostream>

template<template<class...>class>
struct ztag_t {};
    
template <template<class>class T>
constexpr std::size_t template_size_helper(ztag_t<T>) {
     return 1;
}
template <template<class, class>class T>
constexpr std::size_t template_size_helper(ztag_t<T>) {
     return 2;
}


template <typename T>
class MyTemplateClass {  };

template <typename T, typename U>
class MyTemplateClass2 {  };

template<template<class...>class T>
struct template_size:
  std::integral_constant<
    std::size_t,
    template_size_helper(ztag_t<T>{})
  >
{};


int main() {
    std::cout << template_size<MyTemplateClass>::value << "\n";
    std::cout << template_size<MyTemplateClass2>::value << "\n";
}

我不知道有什么方法可以不写出N个重载函数来支持多达N个参数。 实时例子
当然,反射会使这变得微不足道。

在模板 <typename T,typename U = T> class MyTemplateClass3 { } 中,可以使用断点。 - krzikalla
@krzikalla 嗯,那个模板类有歧义,可能是1个或2个参数。你认为哪个是错误答案?正确答案呢? - Yakk - Adam Nevraumont
没有正确答案。它不编译是正确的。 至少在C++17之前,gcc 10会给出答案“2”。但是C++17已经澄清了这个问题。 我只是想指出,上面的解决方案快速简便,但不适用于默认模板参数。 - krzikalla

2

有一个...

° 介绍

正如@Yakk在他对我的另一个答案的评论中指出的那样(没有明确地说),不可能'计算'模板声明的参数的数量。但是,可以'计算'传递给实例化模板的参数的数量。

就像我的另一个答案所展示的那样,计算这些参数相当容易。

所以...
如果不能计算参数...
如何才能在不知道此模板应接收的参数数量的情况下实例化一个模板???

注意
如果你想知道为什么单词实例化(d)在本文中被划掉, 你会在脚注中找到它的解释。所以请继续阅读... ;)

° 搜索过程

  • 如果我们可以以递增的参数数量尝试实例化模板,然后使用 SFINAE(Substitution Failure Is Not An Error)检测失败时,就可以找到一个通用解决方案来解决这个问题... 你不觉得吗?
  • 显然,如果我们想要管理非类型参数,那就没戏了。
  • 但是对于只有 typename 参数的模板...

还是有办法的...

以下是我们应该能够实现它所需的元素:

一个只有使用typename参数声明的模板类可以接收任何类型作为参数。实际上,尽管可以为特定类型定义专门化,但是主模板无法强制其参数的类型
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  • 从C++20概念开始,上述语句可能不再正确。我现在无法尝试,但@Yakk似乎对此非常自信。在他的评论之后:

我认为概念打破了这一点。“主模板无法强制其参数的类型。”是错误的

  • 如果在模板实例化之前应用约束条件,则他可能是正确的。但是...
  • 通过快速跳转到约束和概念的介绍,可以在第一个代码示例之后阅读到:

“违反约束条件会在编译时检测到,在模板实例化过程的早期,这导致易于遵循的错误消息。”

  • 待确认...
  • 完全可以创建一个模板,其唯一目的是与任意数量的参数一起实例化。对于我们这里的用例,它可能仅包含int...(让我们称之为IPack)。

  • 可以定义IPack的成员模板来通过将int添加到当前IPack的参数中来定义下一个IPack。因此,可以逐步增加其参数数量...

  • 这里可能是缺失的部分。这是大多数没有意识到的东西。

    • (我的意思是,当模板访问其参数之一必须具有的成员或者当特征测试存在特定重载时,我们大多数人都经常使用它等等...)

    但我认为有时以不同的方式查看它并说:

    • 可以声明一个由其他类型组装而成的任意类型,其评估可以被编译器延迟直到实际使用它。
    • 因此,将IPack的参数注入到另一个模板中将成为可能...
  • 最后,应该能够使用利用decltypestd::declval的测试特征来检测操作是否成功。(注意:最终没有使用两者之一)

  • ° 构建模块

    步骤1: IPack

    template<typename...Ts>
    struct IPack {
    private:
        template<typename U>    struct Add1 {};
        template<typename...Us> struct Add1<IPack<Us...>> { using Type = IPack<Us..., int>; };
    public:
        using Next = typename Add1<IPack<Ts...>>::Type;
    
        static constexpr std::size_t Size = sizeof...(Ts);
    };
    
    using IPack0 = IPack<>;
    using IPack1 = typename IPack0::Next;
    using IPack2 = typename IPack1::Next;
    using IPack3 = typename IPack2::Next;
    
    constexpr std::size_t tp3Size = IPack3::Size; // 3
    

    现在,一个人有了一种增加参数数量的方法,通过一个方便的方式来获取 IPack 的大小。

    接下来,需要一些东西来构建任意类型,通过将 IPack 的参数注入到另一个模板中。

    步骤 2: IPackInjector

    一个示例,展示了如何将模板的参数注入到另一个模板中。
    它使用模板特化来提取 IPack 的参数,并将其注入到 Target 中。

    template<typename P, template <typename...> class Target>
    struct IPackInjector { using Type = void; };
    
    template<typename...Ts, template <typename...> class Target>
    struct IPackInjector<IPack<Ts...>, Target> { using Type = Target<Ts...>; };
    
    template<typename T, typename U>
    struct Victim;
    
    template<typename P, template <typename...> class Target>
    using IPInj = IPackInjector<P, Target>;
    
    //using V1 = typename IPInj<IPack1, Victim>::Type; // error: "Too few arguments"
    using V2 = typename IPInj<IPack2, Victim>::Type;   // Victim<int, int>
    //using V3 = typename IPInj<IPack3, Victim>::Type; // error: "Too many arguments"
    

    现在,有一种方法可以将IPack的参数注入到Victim模板中,但是,正如您所看到的,如果参数数量与Victim模板的声明不匹配,则直接评估Type会生成错误...

    注意
    您是否注意到Victim模板没有完全定义?
    它不是一个完整的类型。它只是一个模板的前向声明。

    • 要测试的模板不需要是完整的类型,这个解决方案就能按预期工作... ;)

    如果想要能够将这个任意构建的类型传递给某个检测特性,就必须找到一种延迟其评估的方法。 事实证明,这个“技巧”(如果可以这么说)相当简单。

    这与依赖名称有关。你知道那个令人烦恼的规则,它强制你每次访问模板的成员模板时都要添加::template。实际上,这个规则还强制编译器不评估包含依赖名称的表达式,直到它被有效使用...

    • 哦,我明白了!...
      所以,我们只需要准备好IPackInjector而不访问其Type成员,然后将其传递给我们的测试特性,对吧?可以使用类似以下的方式完成:
    using TPI1 = IPackInjector<IPack1, Victim>; // No error
    using TPI2 = IPackInjector<IPack2, Victim>; // No error
    using TPI3 = IPackInjector<IPack3, Victim>; // No error
    

    实际上,上面的例子没有产生错误,并且确认了有一种方法可以准备要构建的类型并在以后评估它们。

    不幸的是,我们无法将这些预配置的类型构建器传递给我们的测试特性,因为我们想使用SFINAE来检测任意类型是否可以被实例化
    而这又与依赖名称有关...

    SFINAE规则可以被利用来使编译器静默地选择另一个模板(或重载),只有当模板中参数的替换是依赖名称时才会发生。
    简而言之:仅适用于当前模板实例的参数。

    因此,为了使检测正常工作而不产生错误,用于测试的任意类型将必须在测试特性内部构建,其中至少有一个参数。测试的结果将被分配给Success成员...

    第三步TypeTestor

    template<typename T, template <typename...> class C>
    struct TypeTestor {};
    
    template<typename...Ts, template <typename...> class C>
    struct TypeTestor<IPack<Ts...>, C>
    {
    private:
        template<template <typename...> class D, typename V = D<Ts...>>
        static constexpr bool Test(int) { return true; }
        template<template <typename...> class D>
        static constexpr bool Test(...) { return false; }
    public:
        static constexpr bool Success = Test<C>(42);
    };
    

    现在,最后,我们需要一台机器来连续尝试使用越来越多的参数实例化我们的Victim模板。有几件事需要注意:

    • 不能声明没有参数的模板,但可以:
      • 只有一个参数包,或者
      • 所有参数都有默认值。
    • 如果测试过程以失败开始,则意味着模板必须采用更多参数。因此,测试必须继续直到成功,然后继续直到第一个失败。
      • 我最初认为使用模板特化的迭代算法可能会有点复杂...但是经过一番思考,发现起始条件并不重要。
      • 只需要检测上一个测试是否为true,下一个测试是否为false
    • 测试次数必须有限制。
      • 确实,模板可以有一个参数包,这样的模板可以接收不确定数量的参数...

    步骤4TemplateArity

    template<template <typename...> class C, std::size_t Limit = 32>
    struct TemplateArity
    {
    private:
        template<typename P> using TST = TypeTestor<P, C>;
    
        template<std::size_t I, typename P, bool Last, bool Next>
        struct CheckNext {
            using PN = typename P::Next;
    
            static constexpr std::size_t Count = CheckNext<I - 1, PN, TST<P>::Success, TST<PN>::Success>::Count;
        };
    
        template<typename P, bool Last, bool Next>
        struct CheckNext<0, P, Last, Next> { static constexpr std::size_t Count = Limit; };
    
        template<std::size_t I, typename P>
        struct CheckNext<I, P, true, false> { static constexpr std::size_t Count = (P::Size - 1); };
    
    public:
        static constexpr std::size_t Max   = Limit;
        static constexpr std::size_t Value = CheckNext<Max, IPack<>, false, false>::Count;
    
    };
    
    template<typename T = int, typename U = short, typename V = long>
    struct Defaulted;
    
    template<typename T, typename...Ts>
    struct ParamPack;
    
    constexpr std::size_t size1 = TemplateArity<Victim>::Value;    // 2
    constexpr std::size_t size2 = TemplateArity<Defaulted>::Value; // 3
    constexpr std::size_t size3 = TemplateArity<ParamPack>::Value; // 32 -> TemplateArity<ParamPack>::Max;
    

    结论

    最后,解决这个问题的算法并不是那么复杂...

    在找到可以解决它的“工具”之后,只是一个常常需要将正确的部分放在正确的位置上的问题... :P

    尽情享受吧!


    ° 重要注释

    这就是为什么单词 实例化(instantiated) 在与 Victim 模板相关的地方被删除的原因。

    单词 实例化(instantiated) 简单来说不是正确的词...

    最好使用尝试声明,或者别名一个未来实例化Victim模板类型。
    (这将会非常无聊):P

    事实上,在这个解决方案的代码中,没有一个Victim模板被实例化...

    作为证明,足以看到在上面的代码中,所有的测试都只是在模板的前向声明上进行的。

    如果你还有疑问...

    using A = Victim<int>;      // error: "Too few arguments"
    using B = Victim<int, int>; // No errors
    
    template struct Victim<int, int>;
    //              ^^^^^^^^^^^^^^^^
    // Warning: "Explicit instantiation has no definition"
    

    最后,介绍中可能会有一句话被删除,因为这个解决方案似乎证明了以下内容:
    • 可以“计算”模板声明的参数数量...
    • 无需实例化此模板。

    我认为这里概念是破解的。 "主模板无法强制其参数类型" 是错误的。 - Yakk - Adam Nevraumont
    @Yakk-AdamNevraumont 我还没有涉及到C++20的概念,但是我已经开发了编译时模板约束特性,例如:Where<T, IsAny<...>, IsNone<..>>... 为了使这些约束生效,需要实例化模板... 而且... 你可能错过了,但是上面的解决方案成功地计算了前向声明的参数数量... 这些模板在成为完整类型之前无法实例化... 因此,我认为可以说这个解决方案完美地回答了“...声明的参数数量”这个问题。或者我错过了什么? - Tenphase
    1
    通过概念,template<Iterator I> 可以约束主模板中的类型 I。因此,你声称它不能在主模板中被约束,只能通过特化来实现,这是不正确的。对于 requires 子句也是如此。举个快速的例子,template<class T> requires (std::is_same_v<T,double>) struct Test; 尝试计算它的参数。 - Yakk - Adam Nevraumont
    @Yakk-AdamNevraumont 我已经在这个_statement上添加了注释。感谢您指出问题,并且对于这个"很好的工作解决方案",太糟糕了。 (我以为是) - Tenphase
    @Yakk-AdamNevraumont 我还没有机会亲自尝试C++20概念,但是在我寻找其他东西时,我花了一点时间阅读了约束和概念的介绍。在那里,明确指出_"违反约束条件会在编译时,在模板实例化过程的早期被检测到..."。这意味着这个解决方案仍然是可能的。不是吗? - 您介意尝试一下以确认您对"我认为概念会破坏这个"_的说法吗? - Tenphase

    0

    ° 在阅读本篇文章之前

    本文不回答“如何获取参数的数量”,
    它回答了“如何获取参数的个数”...

    这篇文章有两个原因留在这里:

    • 它可能会帮助那些混淆了参数参数值含义的人(就像我一样)。
    • 本文使用的技术与我发布的另一个答案中使用的技术密切相关
      用于生成正确答案

    请参见我的其他答案,以查找模板的“参数数量”。


    Elliott的答案更像是通常所做的(尽管我认为主模板应该完全定义并“做些什么”)。它使用了一个模板特化,用于当模板作为参数传递给主模板时。

    与此同时,Elliott的答案消失了...
    因此,我发布了类似于他下面展示的内容。
    (请参见“通用解决方案”)

    但是,只是为了向您展示您离一个可行的解决方案并不远,并且,因为我注意到您已经使用了一个函数来尝试,而且,您声明了这个函数constexpr,您可以这样编写它:

    注意
    这是一种“花哨的解决方案”,但它有效...

    template <typename T>             class MyTemplateClass {};
    template <typename T, typename U> class MyTemplateClass2 {};
    
    template <template <typename...> class T, typename...Ts>
    constexpr const size_t template_size(T<Ts...> && v)
    {
        return sizeof...(Ts);
    }
    
    // If the target templates are complete and default constructible.
    constexpr size_t size1 = template_size(MyTemplateClass<int>{});
    constexpr size_t size2 = template_size(MyTemplateClass2<int, short>{});
    
    // If the target templates are complete but NOT default constructible.
    constexpr size_t size3 = template_size(decltype(std::declval<MyTemplateClass<int>>()){});
    constexpr size_t size4 = template_size(decltype(std::declval<MyTemplateClass2<int, short>>()){});
    
    说明
    你说了"因为Ts无法被引用",这是正确错误的,因为你声明template_size的方式不同。
    也就是说,模板模板参数本身不能声明参数(在函数模板的声明中放置Ts的位置)。可以这样做是为了给出模板参数应该接收的参数的提示,但它 不是 当前模板声明的参数名称...
    (我希望足够清楚) ;)

    显然,这可能有点过于复杂,但我认为值得知道这样的结构也是可能的... ;)

    通用解决方案

    template <typename T>             class MyTemplateClass {};
    template <typename T, typename U> class MyTemplateClass2 {};
    
    template<typename T>
    struct NbParams { static constexpr std::size_t Value = 0; };
    
    template<template <typename...> class C, typename...Ts>
    struct NbParams<C<Ts...>> { static constexpr std::size_t Value = sizeof...(Ts); };
    
    constexpr size_t size1 = NbParams<MyTemplateClass<int>>::Value;
    constexpr size_t size2 = NbParams<MyTemplateClass2<int, short>>::Value;
    
    

    这是处理这种事情的常规方式... ;)


    1
    这个计数传递给模板的参数数量,而不是模板接受的参数数量? - Yakk - Adam Nevraumont
    +1 @Yakk-AdamNevraumont 没错。我被“计算任何类的模板参数数量”这句话所迷惑,没有足够关注函数如何使用template_size<MyTemplateClass>() == 1。最终,这个“表面上简单的问题”并不像它看起来那么简单... ;) - Tenphase
    @Yakk-AdamNevraumont 最后我可以说:“有一个……”看看我的另一个答案(如果你对这个问题的解决方案感兴趣)。 - Tenphase

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接