Counting parameters of a template template type

Viewed 665

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;
}
5 Answers

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.

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.

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.

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!

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);
}
Related