Counting parameters of a template template type
Asked Answered
M

6

9

Following code works in GCC (at least in GCC 10.1.0), but not in MSVC and Clang. I'm not sure if it's legal in C++ standard.

I'm trying to count the parameters in a template template type.

Is the following code a valid C++ code, if yes, then how to make them work in Clang and MSVC, if not, is there an alternative to this?

Code on compiler explorer

template <template<typename...> typename T>
struct template_count {
    static constexpr unsigned value = 0;
};

template <template<typename> typename T>
struct template_count<T> {
    static constexpr unsigned value = 1;
};

template <template<typename, typename> typename T>
struct template_count<T> {
    static constexpr unsigned value = 2;
};

template <template<typename, typename, typename> typename T>
struct template_count<T> {
    static constexpr unsigned value = 3;
};


template <typename one, typename two, typename three>
struct test {

};

int main() {
    return template_count<test>::value;
}
Maemaeander answered 13/8, 2020 at 16:6 Comment(9)
What is template_count expected to do with template<typename = void> struct test{}; and template<typename...> struct test{};?Paigepaik
@Paigepaik a default 0 or -1 is good enough; or even a "is_valid" field.Maemaeander
If you found a solution which works, I suggest posting as an answer. An answer should go to answer box.Dittman
@Dittman The remaining question is how can we go beyond 10! And also I haven't got an answer for if the first code is standard or mistakenly works in GCC (or even the second code)Maemaeander
@moisrex Hmmm... Since I feel like, your question is a bunch of other questions, I would suggest, focus on one issue and post the other (e.g "beyond 10") as another one!Dittman
@Dittman I moved the solution to an answer, but I keep my eye for more answers here. no rushMaemaeander
Is there a practical use case here, or are you just curious? Every practical use case I can think of has additional restrictions on what answer you end up getting, and those additional restrictions end up generating better solutions. Practically, the above program can be replaced with return 3;; I understand you may have reduced a different problem to if I have a solution to this problem, but what I'm asking for is the problem you reduced to this.Therein
@Yakk-AdamNevraumont I needed to this a while ago where I needed this in my project's extension system (gitlab.com/webpp/webpp) to identify what type of extension is the extension; the difference between extensions is their template parameter count. 1 is a mother extension, 2 is a child extension which requires more love :)Maemaeander
@moisrex That is a much simpler problem than the one presented above. Remember, it goes 1, 2, infinity not 1 infinity. ;)Therein
M
2

After a hint from someone in twitter, I found a better solution that works on GCC and Clang (Link to compiler explorer) (in github):

#include <type_traits>
#include <cstddef>

struct variadic_tag {};
struct fixed_tag : variadic_tag {};
struct number_signature {};

template<std::size_t V>
struct ParameterNumber : number_signature {
    constexpr static auto value = V;
};

template<typename T>
concept NumberObjConcept = std::is_base_of_v<number_signature, T>;

template<template<typename> typename>
auto DeduceArgs() -> ParameterNumber<1>;

template<template<typename, typename> typename>
auto DeduceArgs() -> ParameterNumber<2>;

template<template<typename, typename, typename> typename>
auto DeduceArgs() -> ParameterNumber<3>;

template<template<typename, typename, typename, typename> typename>
auto DeduceArgs() -> ParameterNumber<4>;

template<template<typename, typename, typename, typename, typename> typename>
auto DeduceArgs() -> ParameterNumber<5>;

template<template<typename, typename, typename, typename, typename, typename> typename>
auto DeduceArgs() -> ParameterNumber<6>;

template<template<typename, typename, typename, typename, typename, typename, typename> typename>
auto DeduceArgs() -> ParameterNumber<7>;

template<template<typename, typename, typename, typename, typename, typename, typename, typename> typename>
auto DeduceArgs() -> ParameterNumber<8>;

template<template<typename, typename, typename, typename, typename, typename, typename, typename, typename> typename>
auto DeduceArgs() -> ParameterNumber<9>;

template<template<typename, typename, typename, typename, typename, typename, typename, typename, typename, typename> typename>
auto DeduceArgs() -> ParameterNumber<10>;

template<template<typename...> typename F>
auto DeduceTemplateArgs(variadic_tag) -> ParameterNumber<1000000000>; // a meaningless big number

template<template<typename...> typename F>
auto DeduceTemplateArgs(fixed_tag)    -> decltype(DeduceArgs<F>());

template <typename one, typename two, typename three>
struct test {

};

#define __DEDUCE_TEMPLATE_ARGS(c) decltype(DeduceTemplateArgs<c>(fixed_tag{}))

int main() {
    return __DEDUCE_TEMPLATE_ARGS(test)::value;
}

of course the above code fails on such situations:

template <template<typename> typename>
struct test {};

and that's fine for most situations.

Maemaeander answered 13/8, 2020 at 18:40 Comment(1)
I have never been in the situation that I had to use more than 10 template-template args. So if I were you, will accept this. However, we have very talented people around the world, who can think one more step further. So being optimistic isn't bad at all. BTW +1 for having a better sln.Dittman
F
2

For variadic templates such as std:tuple, it is impossible to calculate the number of types it can accept, but once we set a maximum count number such as 50, it will become relatively easy.

The basic idea is to try to instantiate the template template parameters from the maximum number of types (here we just use void) and decrease one by one until it succeeds.

#include <utility>

template<std::size_t>
using void_t = void;

template<template<class...> class C, std::size_t... Is>
constexpr std::size_t template_count_impl(std::index_sequence<Is...>) {
  constexpr auto n = sizeof...(Is);
  if constexpr (requires { typename C<void_t<Is>...>; })
    return n;
  else
    return template_count_impl<C>(std::make_index_sequence<n - 1>{});
}

template<template<class...> class C>
constexpr std::size_t template_count() {
  constexpr std::size_t max_count = 50;
  return template_count_impl<C>(std::make_index_sequence<max_count>{});
}

template<class> struct A {};
template<class,class=int> struct B {};
template<class,class,class> struct C {};
template<class,class,class,class> struct D {};
template<class,class,class,class,class=int> struct E {};

static_assert(template_count<A>() == 1);
static_assert(template_count<B>() == 2);
static_assert(template_count<C>() == 3);
static_assert(template_count<D>() == 4);
static_assert(template_count<E>() == 5);

Demo.

Frigate answered 14/10, 2021 at 2:32 Comment(4)
it is impossible to calculate the number of types it can accept I actually realised today that we can calculate it. See here.Abase
using T = C<Ts...>. Oh, that's a good trick.Furl
Actually, here's an example where both our solutions fail drastically: template <class T> concept ForceFail = T::forcefail; template <class T> requires ForceFail<T> class tricky;Abase
Yes, it is impossible to handle all cases. If NTTP such as template <auto, class, class...> is introduced, things will become more complicated.Furl
A
2

What's the problem?

All information that we'd want to know regarding the "count template arguments" problem:

  1. How many non-default arguments?
  2. How many non-variadic arguments (ie. min arguments)?
  3. Is it variadic?

Example:

template <class, class = foo, class...> bar;
  1. non-default args = 1
  2. minimum arguments (non-variadic args) = 2
  3. is variadic = yes

Solution (idea)

  1. Much like in 康桓瑋's solution, we fill the template class (C) with a list of types (it doesn't matter the type [EDIT: see Note! below]) and check if it abides by C's declaration (by using SFINAE/requires), if not we keep incrementing the size of the list - However, in our case we'll increment up starting from an empty list. Once we've found a list big enough, then we'll know the number of non-default arguments.

  2. Once we have a list to fill all the non-default arguments, we can put those into C: using T = C<Ts...>. C will automatically fill the default arguments, which are now contained in T. All we need to do is send it to a type trait to sizeof... the arguments and we've got the minimum number of arguments.

  3. Finally, we check if T can take one more type argument: if it can, it's a variadic!


The code

template <auto v>
struct tag_v
{
    static constexpr auto value = v;
};

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

template <class>
struct size_of_pack;

template <template <class...> class P, class ... Ts>
struct size_of_pack <P<Ts...>>
    : tag_v<sizeof...(Ts)> {};

template <class P>
constexpr int size_of_pack_v = size_of_pack<P>::value;

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

template <class>
struct is_variadic;

template <template <class...> class P, class ... Ts>
struct is_variadic <P<Ts...>>
    : tag_v<requires {typename P<Ts..., int>;}> {};

template <class P>
constexpr bool is_variadic_v = is_variadic<P>::value;

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

struct template_class_args_info
{
    int n_non_default;
    int min_args;
    bool variadic;
};

template <template <class...> class C, class ... Ts>
constexpr auto get_template_class_args_info ()
{
    // We can invoke C with any types.
    // Here we choose "int"

    if constexpr (requires {typename C<Ts...>;})
    {
        using T = C<Ts...>; // C auto-fills the default args

        return template_class_args_info{
            sizeof...(Ts),
            size_of_pack_v<T>,
            is_variadic_v<T>};
    }
    else
    {
        return get_template_class_args_info<C, Ts..., int>();
    }
}

Demos: c++20, c++11


Note!

Pre-c++20:

The number of non-default arguments calculated has some limitations when the type declaration uses SFINAE:

template <class T, class = typename T::force_fail>
class tricky1;

The answer to n_non_default should be 1, but when we try tricky1<int> it's SFINAE-d, so we incorrectly provide 2 as the answer.


After c++20:

concepts actually makes this problem much more difficult, consider:

template <class T>
concept ForceFail = requires {typename T::force_fail;};

template <class T>
requires ForceFail<T> 
class tricky2;

Similar to the previous example, we're restricting the type of the argument, but unlike last time we can't override it by adding another type, hence we can no longer fill class templates with just anything. Needless to say, get_template_class_args_info<trick2>() fails to compile altogether!

Abase answered 14/10, 2021 at 9:53 Comment(0)
T
1

This uses value-based metaprogramming.

tag(_t) is a compile time value representing a type.

template<class T>
struct tag_t { using type=T; };
template<class T, T t>
struct tag_t<std::integral_constant<T,t>>:std::integral_constant<T,t> { using type=T; };
template<class T>
constexpr tag_t<T> tag = {};

value(_t) is a compile-time value (and a tag).

template<auto x>
using value_t = tag_t<std::integral_constant<std::decay_t<decltype(x)>, x>>;

template<auto x>
constexpr value_t<x> value = {};

ztemplate(_t)<Z> is a compile time value representing a template.

template<template<class...>class Z, std::size_t N=0>
struct ztemplate_type : value_t<N>, ztemplate_type<Z, static_cast<std::size_t>(-1)> {};

template<template<class...>class Z>
struct ztemplate_type<Z, static_cast<std::size_t>(-1)>:
  tag_t<ztemplate_type<Z, static_cast<std::size_t>(-1)>>
{
    template<class...Ts>
    constexpr tag_t<Z<Ts...>> operator()( tag_t<Ts>... ) const { return {}; }
};

template<template<class>class Z>
constexpr ztemplate_type<Z,1> ztemplate_map( ztemplate_type<Z>, int ) { return {}; }
template<template<class,class>class Z>
constexpr ztemplate_type<Z,2> ztemplate_map( ztemplate_type<Z>, int ) { return {}; }
template<template<class,class,class>class Z>
constexpr ztemplate_type<Z,3> ztemplate_map( ztemplate_type<Z>, int ) { return {}; }
template<template<class...>class Z>
constexpr ztemplate_type<Z> ztemplate_map( ztemplate_type<Z>, ... ) { return {}; }

template<template<class...>class Z>
using ztemplate_t = decltype( ztemplate_map( ztemplate_type<Z>{}, true ) );
template<template<class...>class Z>
constexpr ztemplate_t<Z> ztemplate = {};

don't use ztemplate_type directly; use ztemplate_t and ztemplate.

Test code:

template<class> struct A {};
template<class,class> struct B {};
template<class,class,class> struct C {};
template<class,class,class,class> struct D {};

auto a = ztemplate<A>;
auto b = ztemplate<B>;
auto c = ztemplate<C>;
auto d = ztemplate<D>;
(void)a, (void)b, (void)c, (void)d;

auto a_v = a(tag<int>);
auto b_v = b(tag<int>, tag<double>);
auto c_v = c(tag<int>, tag<double>, tag<char>);
auto d_v = d(tag<int>, tag<double>, tag<char>, tag<void>);
(void)a_v, (void)b_v, (void)c_v, (void)d_v;
std::cout << a << b << c << "\n";

output is:

 123

Live example.

In this paradigm, a ztemplate is a function object that maps tags. Those tags could be ztemplates or values or wrap raw types.

Therein answered 14/8, 2020 at 19:3 Comment(0)
P
-1

You can use a specific template instance.

template <typename T>
struct template_count { static constexpr unsigned value = 0; };

template <template<typename...> typename T, typename ... Args>
struct template_count<T<Args...>> { static constexpr unsigned value = sizeof...(Args); };

int main() {
    static_assert(template_count<test<int,int,int>>::value == 3);
}

The general solution without the particular instantiation might have problems with variadic templates.

template <typename ... T> struct test {};

int main() {
    static_assert(template_count<test>::value == ????); //no solution
    static_assert(template_count<test<int,int>>::value == 2);
}
Pollaiuolo answered 14/8, 2020 at 15:0 Comment(1)
Counting a typename is easy enough. The question is to count parameters of a template template.Maemaeander
B
-1

For those who want C++11 version without using fancy concepts or requires clause:

// Get the template arity for a given type C. 
// MaxSize can be set by the user to avoid infinite search
template<template<typename...> class C, std::size_t MaxSize = 99>
struct template_arity;

// Get the default template arity for a given type C.
template<template<typename...> class C, std::size_t MaxSize = 99>
struct default_template_arity;

// Get mandatory template arity for a given type C
template<template<typename...> class C>
struct mandatory_template_arity;

Demo

https://godbolt.org/z/4n84r64eT

#include <functional>
#include <map>
#include <tuple>
#include <type_traits>
#include <vector>

template<typename...>
using void_t = void;

template<typename Tuple1, typename Tuple2> struct tuple_cat_type_impl;
template<typename... Ts, typename... Us>   struct tuple_cat_type_impl<std::tuple<Ts...>, std::tuple<Us...>> { using type = std::tuple<Ts..., Us...>; };
template<typename Tuple1, typename Tuple2> using tuple_cat_t = typename tuple_cat_type_impl<Tuple1, Tuple2>::type;

template<typename T, std::size_t N> struct type_sequence_impl;
template<typename T> struct type_sequence_impl<T, 0> { using type = std::tuple<>; };
template<typename T> struct type_sequence_impl<T, 1> { using type = std::tuple<T>; };
template<typename T> struct type_sequence_impl<T, 2> { using type = std::tuple<T, T>; };
template<typename T, std::size_t N>
struct type_sequence_impl {
  using type = tuple_cat_t<typename type_sequence_impl<T, (N / 2)>::type, typename type_sequence_impl<T, N - (N / 2)>::type>;
};
template<typename T, std::size_t N>
using type_sequence = typename type_sequence_impl<T, N>::type;

template<template<typename...> class C, typename ArgTuple, typename = void>
struct can_put_template : std::false_type {};
template<template<typename...> class C, typename... Args>
struct can_put_template<C, std::tuple<Args...>, void_t<C<Args...>>> : std::true_type {};

template<template<typename...> class C, std::size_t N, bool = can_put_template<C, type_sequence<int, N>>::value>
struct mandatory_template_arity_impl;

template<template<typename...> class C, std::size_t N> struct mandatory_template_arity_impl<C, N, true> : std::integral_constant<std::size_t, N> {};
template<template<typename...> class C, std::size_t N> struct mandatory_template_arity_impl<C, N, false> : mandatory_template_arity_impl<C, N + 1> {};

template<template<typename...> class C>
struct mandatory_template_arity : mandatory_template_arity_impl<C, 0> {};

template<template<typename...> class C, std::size_t Size, bool = can_put_template<C, type_sequence<int, Size>>::value /* true */>
struct template_arity_impl;
template<template<typename...> class C, std::size_t Size> struct template_arity_impl<C, Size, true> : std::integral_constant<std::size_t, Size> {};
template<template<typename...> class C, std::size_t Size> struct template_arity_impl<C, Size, false> : template_arity_impl<C, Size - 1> {};
template<template<typename...> class C>                   struct template_arity_impl<C, 0,    false> : std::integral_constant<std::size_t, std::size_t(-1)> {};

template<template<typename...> class C, std::size_t MaxSize = 6>
struct template_arity : template_arity_impl<C, MaxSize>{};

template<template<typename...> class C, std::size_t MaxSize = 6>
struct default_template_arity
    : std::integral_constant<std::size_t, template_arity<C>::value - mandatory_template_arity<C>::value> {};

template<typename = void>
struct cxx14_less {};

int main() {
  static_assert(template_arity<std::vector>::value == 2, "");
  static_assert(mandatory_template_arity<std::vector>::value == 1, "");

  static_assert(template_arity<std::map>::value == 4, "");
  static_assert(mandatory_template_arity<std::map>::value == 2, "");

  static_assert(template_arity<cxx14_less>::value == 1, "");
  static_assert(mandatory_template_arity<cxx14_less>::value == 0, "");


  return 0;
}
Bridges answered 12/1, 2024 at 14:25 Comment(5)
Downvoting because it doesn't add anything new. There was already c++11 demos and more generic solutions than this (no max size needed) - this also lacks any comments...Abase
@Abase No. Other answers don't work in C++11. And this solution doesn't have a size limit. MaxSize is user-settable. It is to reduce compile-time. I've added some explanation.Gastropod
That won't help with compile-time issues. Yes, my solution (and probably others) work in c++11. See the demo code link specifically for that.Abase
@Abase I mean the compiling time itself. Other answers(except yours) rely on if constexpr and template variable which are not allowed in C++ 11. TBH, I didn't noticed your C++11 demo link. Why not sharing the code on your post? Thank you for your comment.Gastropod
Like mine, yours counts up till it finds an answer. Having a max is an unnecessary hack that will in no way help with compile times. I'd be more concerned with how you're concatenating std::tuple: you're recursively building a tuple of ints each time you're checking a size. It's faster to add an int to the type you already have. And I didn't post the c++11 code as well as the c++20 code because translating between them is quite trivial.Abase

© 2022 - 2025 — McMap. All rights reserved.