Kamajii
Kamajii

Reputation: 1878

Deduce/erase type of template template argument

When using template template arguments how can I have the template type of the template template deduced or erased?

Consider the following SSCCE:

#include <cstdint>
#include <cstddef>
#include <iostream>
using namespace std;

template<int i>
struct Value { };

template<int i>
struct BadValue { };

template<typename... G>
struct Print;

template<template<int> class ValueType, int... Is>
struct Print< ValueType<Is>... > {
    static void print() {
        const int is[] = { Is... };
        for (int i: is)
            cout << i;
        cout << endl;
    }

};

using V1 = Value<1>;
using V2 = Value<2>;
using V3 = Value<3>;
using BV = BadValue<1>;

int main() {
    Print<V2, V1, V2, V3>::print(); // <-- fine
    Print<V2, V1, V2, BV>::print(); // <-- BV used by accident
}

Deducing the template<int> class ValueType argument of the Print class to a template class like the Value and BadValue classes enforces that all the template arguments in the parameter pack to the Print class are specializations of the same ValueType template class - this is intentional. That is, the second line in the main() function causes a compile-time error as the ValueType argument cannot be deduced to match both the Value and BadValue classes. If the user by accident tries to mix the templates when using the Print template a compile time error arises, which provides a bit of diagnostic.

The above implementation, however, still has the int type fixed for the inner template argument of the ValueType template template argument. How can I erase it and have it deduced as well?

Generally speaking, when deducing a template template argument, how can I access the inner template argument?

Upvotes: 2

Views: 285

Answers (2)

max66
max66

Reputation: 66230

If I understand correctly, you want that Print<V2, V1, V2, VB>::print(); generate an error that is simpler to understand.

For this, the best I can imagine is to works with static_assert()s.

In this particular case -- Print is a struct with only a partial specialization implemented and no general version implemented -- a not really but simple solution is available: implement the general version to give a static_assert() error with a message of your choice.

By example

template <typename ... G>
struct Print
 {
   static_assert( sizeof...(G) == 0, "not same int container for Print<>");

   static void print()
    { };
 };

template <template<int> class ValueType, int ... Is>
struct Print< ValueType<Is>... >
 {
   static void print()
    {
      using unused = int const [];

      (void)unused { (std::cout << Is, 0)... };

      std::cout << std::endl;
    }
 };

Unfortunately this solution accept as valid Print<>; I don't know if is good for you.

Another (better, IMHO, but more elaborate) solution can be transform the Print partial specialization in a specialization that accept variadic int containers (variadic ValueTypes instead a fixed ValueType) and, in a static_assert(), check (with a custom type traits) that all containers are the same.

Bye example, with the following custom type traits

template <template <int> class ...>
struct sameCnts : public std::false_type
 { };

template <template <int> class C0>
struct sameCnts<C0> : public std::true_type
 { };

template <template <int> class C0, template <int> class ... Cs>
struct sameCnts<C0, C0, Cs...> : public sameCnts<C0, Cs...>
 { };

you can write the Print specialization as follows

template <template <int> class ... Cs, int ... Is>
struct Print< Cs<Is>... >
 {
   static_assert(sameCnts<Cs...>{}, "different containers in Print<>");

   static void print()
    {
      using unused = int const [];

      (void)unused { (std::cout << Is, 0)... };

      std::cout << std::endl;
    }
 };

If you can use C++17, you can use folding and the type traits can be written

template <template <int> class, template <int> class>
struct sameCnt : public std::false_type
 { };

template <template <int> class C>
struct sameCnt<C, C> : public std::true_type
 { };

template <template <int> class C0, template <int> class ... Cs>
struct sameCnts
   : public std::bool_constant<(sameCnt<C0, Cs>::value && ...)>
 { };

and (using folding also in print() method) Print as follows

template <template <int> class ... Cs, int ... Is>
struct Print< Cs<Is>... >
 {
   static_assert( sameCnts<Cs...>{}, "different containers in Print<>");

   static void print()
    { (std::cout << ... << Is) << std::endl; }
 };

-- EDIT --

The OP ask

But how can I have the Print class accept also, for example, types that are specialized for a double non-type value instead of the int non-type values?

Not sure to understand what do you want but (remembering that a double value can't be a template non-type parameter) I suppose you want a Print that accept types with non-types template parameter when the type of this non type template parameter isn't fixed as in your example (int).

For C++11 and C++14 I think that in necessary to explicit the type of the non type values.

I mean... If you write Print as follows

template <typename ...>
struct Print;

template <typename T, template <T> class ... Cs, T ... Is>
struct Print< T, Cs<Is>... >
 {
   static_assert(sameCnts<Cs...>{}, "different containers in Print<>");

   // ...
 };

you have to use it this way

Print<int, V2, V1, V2, V3>::print();

that is explicating int (or long, or whatever) as first template parameter. This because the int type can't be deduced.

Starting from C++17 you can use auto as type for non-type template parameter, so you can write Print as follows

template <typename ...>
struct Print;

template <template <auto> class ... Cs, auto ... Is>
struct Print< Cs<Is>... >
 {
   static_assert( sameCnts<Cs...>{}, "different containers in Print<>");

   static void print()
    { (std::cout << ... << Is) << std::endl; }
 }; 

and the is no need to explicit the type and you can write

Print<V2, V1, V2, V3>::print();

In this case, you have to use auto instead of int also in sameCnt and sameCnts.

Upvotes: 1

xskxzr
xskxzr

Reputation: 13040

If you work in C++17, you can declare non-type template parameter with auto, so simply declare Is as auto..., and use auto instead of int in the function definition as possible as you can.

Of course, since type of elements of Is may be different, it may be impossible to declare the array is. Instead, you can use std::tuple and print the tuple instead.

// print_tuple is used to print a tuple

template<std::size_t I = 0, typename... Tp>
inline typename std::enable_if<I == sizeof...(Tp)>::type
  print_tuple(const std::tuple<Tp...>&)
  { }

template<std::size_t I = 0, typename... Tp>
inline typename std::enable_if<I < sizeof...(Tp)>::type
  print_tuple(const std::tuple<Tp...>& t)
  {
    std::cout << std::get<I>(t);  
    print_tuple<I + 1, Tp...>(t);
  }

// ...

template<template<int> class ValueType, auto... Is>
                                     // ^^^^
struct Print< ValueType<Is>... > {
    static void print() {
        print_tuple(std::make_tuple(Is...)); // make a tuple, and print it
    }
};

LIVE EXAMPLE

The above pattern (making a tuple then dealing with the tuple) allows you to apply some complicated function to the parameter pack Is. However, if you only want to print the pack, you can alternatively use the C++17 feature fold expression instead, which is simpler.

template<template<int> class ValueType, auto... Is>
                                     // ^^^^
struct Print< ValueType<Is>... > {
    static void print() {
        (std::cout << ... << Is); // fold expression, also C++17 feature
    }
};

LIVE EXAMPLE

Upvotes: 0

Related Questions