c++c++11template-templates

Get number of template parameters with template template function


I'm not sure if this is possible, but I would like to count the number of template arguments of any class like:

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

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

such that template_size<MyTemplateClass>() == 1 and template_size<MyTemplateClass2>() == 2. I'm a beginner to template templates, so I came up with this function which of course does not work:

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

because Ts can not be referenced. I also know that it might come to problems when handling variantic templates, but that is not the case, at least for my application.

Thx in advance


Solution

  • There is one...

    ° Introduction

    Like @Yakk pointed out in his comment to my other answer (without saying it explicitly), it is not possible to 'count' the number of parameters declared by a template. It is, on the other hand, possible to 'count' the number of arguments passed to an instantiated template.

    Like my other answer shows it, it is rather easy to count these arguments.

    So...
    If one cannot count parameters...
    How would it be possible to instantiate a template without knowing the number of arguments this template is suppose to receive ???

    Note
    If you wonder why the word instantiate(d) has been stricken throughout this post, you'll find its explanation in the footnote. So keep reading... ;)

    ° Searching Process

    There is one...

    Here are the elements with which one should be able to make it possible:

    1. A template class declared with only typename parameters can receive any type as argument. Indeed, although there can have specializations defined for specific types,
      a primary template cannot enforce the type of its arguments.
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

      • The above statement might no longer be true from C++20 concepts. I cannot try ATM, but @Yakk seems rather confident on the subject. After his comment:

      I think concepts breaks this. "a primary template cannot enforce the type of its arguments." is false.

      • He might be right if constraints are apply before the template instantiation. But...
      • By doing a quick jump to the introduction to Constraints and concepts, one can read, after the first code example:

      "Violations of constraints are detected at compile time, early in the template instantiation process, which leads to easy to follow error messages."

      • To be confirmed...
    2. It is perfectly possible to create a template having for sole purpose to be instantiated with any number of arguments. For our use case here, it might contain only ints... (let's call it IPack).

    3. It is possible to define a member template of IPack to define the Next IPack by adding an int to the arguments of the current IPack. So that one can progressively increase its number of arguments...

    4. Here is maybe the missing piece. It is maybe something that most people don't realize.

      • (I mean, most of us uses it a lot with templates when, for example, the template accesses a member that one of its arguments must have, or when a trait tests for the existence of a specific overload, etc...)

      But I think it might help in finding solutions sometimes to view it differently and say:

      • It is possible to declare an arbitrary type, built by assembling other types, for which the evaluation by the compiler can be delayed until it is effectively used.
      • Thus, it will be possible to inject the arguments of an IPack into another template...
    5. Lastly, one should be able to detect if the operation succeeded with a testing trait making use of decltype and std::declval. (note: In the end, none of both have been used)

    ° Building Blocks

    Step 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
    

    Now, one has a means to increase the number of arguments,
    with a convenient way to retrieve the size of the IPack.

    Next, one needs something to build an arbitrary type
    by injecting the arguments of the IPack into another template.

    Step 2: IPackInjector

    An example on how the arguments of a template can be injected into another template.
    It uses a template specialization to extract the arguments of an IPack,
    and then, inject them into the 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"
    

    Now, one has a means to inject the arguments of an IPack into a Victim template, but, as one can see, evaluating Type directly generates an error if the number of arguments does not match the declaration of the Victim template...

    Note
    Have you noticed that the Victim template is not fully defined ?
    It is not a complete type. It's only a forward declaration of a template.

    • The template to be tested will not need to be a complete type
      for this solution to work as expected... ;)

    If one wants to be able to pass this arbitrary built type to some detection trait one will have to find a way to delay its evaluation. It turns out that the 'trick' (if one could say) is rather simple.

    It is related to dependant names. You know this annoying rule that enforces you to add ::template everytime you access a member template of a template... In fact, this rule also enforces the compiler not to evaluate an expression containing dependant names until it is effectively used...

    using TPI1 = IPackInjector<IPack1, Victim>; // No error
    using TPI2 = IPackInjector<IPack2, Victim>; // No error
    using TPI3 = IPackInjector<IPack3, Victim>; // No error
    

    Indeed, the above example does not generate errors, and it confirms that there is a means to prepare the types to be built and evaluate them at later time.

    Unfortunately, it won't be possible to pass these pre-configured type builders to our test trait because one wants to use SFINAE to detect if the arbitrary type can be instantiated or not.
    And this is, once again, related to dependent name...

    The SFINAE rule can be exploited to make the compiler silently select another template (or overload) only if the substitution of a parameter in a template is a dependant name.
    In clear: Only for a parameter of the current template instantiation.

    Hence, for the detection to work properly without generating errors, the arbitrary type used for the test will have to be built within the test trait with, at least, one of its parameters. The result of the test will be assigned to the Success member...

    Step 3: 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);
    };
    

    Now, and finally, one needs a machinery that will successively try to instantiate our Victim template with an increasing number of arguments. There are a few things to pay attention to:

    Step 4: TemplateArity

    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;
    

    ° Conclusion

    In the end, the algorithm to solve the problem is not that much complicated...

    After having found the 'tools' with which it would be possible to do it, it only was a matter, as very often, of putting the right pieces at the right places... :P

    Enjoy !


    ° Important Footnote

    Here is the reason why the word intantiate(d) has been stricken at the places where it was used in relation to the Victim template.

    The word instantiate(d) is simply not the right word...

    It would have been better to use try to declare, or to alias the type of a future instantiation of the Victim template.
    (which would have been extremely boring) :P

    Indeed, none of the Victim templates gets ever instantiated within the code of this solution...

    As a proof, it should be enough to see that all tests, made in the code above, are made only on forward declarations of templates.

    And if you're still in doubt...

    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"
    

    In the end, there's a full sentence of the introduction which might be stricken, because this solution seems to demonstrate that: