A Template Argument Extender

Overview

When writing C++ code, one may come across the need to have a set of N arguments of all identical types for a variadic template type. In other words, it would be nicer to write:

typedef type_extend<0, std::tuple, std::string>::type t0;
typedef type_extend<1, std::tuple, std::string>::type t1;
typedef type_extend<2, std::tuple, std::string>::type t2;
typedef type_extend<3, std::tuple, std::string>::type t3;
typedef type_extend<4, std::tuple, std::string>::type t4;

instead of:

typedef std::tuple<> t0;
typedef std::tuple<std::string> t1;
typedef std::tuple<std::string, std::string> t2;
typedef std::tuple<std::string, std::string, std::string> t3;
typedef std::tuple<std::string, std::string, std::string, std::string> t4;

In other words, the goal is to have a positive integer (i.e., the first parameter to type_extend) that determines how many times the last argument passed to type_extend will be duplicated in an argument list that will be applied as the template arguments to the type denoted by the second parameter passed to type_extend.

This article explores how to do this using variadic templates which will be part of the upcoming C++ standard.


Template Template Parameters

In the following code, notice the use of std::tuple appears without any template arguments even though std::tuple requires a list of types as its template arguments:

typedef type_extend<2, std::tuple, std::string>::type t2;

C++ allows the programmer to specify that template arguments are placeholders for class templates. Such placeholders are called template template parameters and are declared the same as class templates except that struct and union cannot be used:

template <template<typename X> class T> // OK
void some_func(T<float>*);

template <template<typename X> typename T> // ERROR
void some_func(T<double>*);

template <template<typename X> struct T> // ERROR
void some_func(T<short>*);

template <template<typename X> union T> // ERROR
void some_func(T<long>*);

Notice that the above code allows the user to specify a class that has only one template argument without specifying the template argument:

template <typename N>
struct Number
{
  N num;
};

template <template <typename> class T>
struct SomeClass
{
  // This class sets the argument to T as float...
  T<float> something;
};

int main()
{
  // Notice Number has no arguments...
  typedef SomeClass<Number> N;

  N n;
  n.something.num = 3.14F; // NOTE: num is a float
}

The usefulness of template template parameters is to allow one to omit the template arguments when passing a class template type as a template argument. The downside in using template template parameters are that (a) any use must fully specify all template parameters to the template template placeholder name and (b) the number of template arguments must exactly match the type being passed in. The restriction that (b) places is important to note: the STL containers all have default arguments and therefore they all have at least two arguments –not one!


Implementing type_extend

In order to build up a variable argument list of types, two class templates will be defined: type_extend and type_extend_impl. The type_extend class templates have the three template arguments previously discussed. The type_extend_impl class templates will need to build up the list of template arguments up to the number specified. The purpose of type_extend is to handle the N-th case and the 0-th (i.e., base) case and its definition is straight-forward:

template <unsigned N, template <typename...> class T, typename Arg>
struct type_extend
{
  typedef typename type_extend_impl<N-1,T,Arg>::type type;
};

template <template <typename...> class T, typename Arg>
struct type_extend<0,T,Arg>
{
  typedef T<> type;
};

In the base case (i.e., in the template specialization type_extend<0,T,Arg>), the computed type is T without any template arguments. In all other cases, the computed type is determined by the definition of type in type_extend_impl.


Implementing type_extend_impl

Since the only way to go from some value of an integer N as a template argument to 0 is to use recursion, the first step is to determine what is the base case's type result that needs to be computed. Since the type_extend class template handled the case where there were no arguments, then the base case of type_extend_impl will need to be T having only one template argument. As for all other cases, the number of arguments needs to be increased by one per recursive iteration. To following code accomplishes this:

template <
  unsigned N,
  template <typename...> class T,
  typename Arg,
  typename... Args
>
struct type_extend_impl
{
  // This class was invoked with Arg, Args... and to increase the
  // Arg one more time we need to add it again which is why Arg
  // appears twice below...
  typedef
    typename type_extend_impl<N-1,T,Arg,Arg,Args...>::type
    type
  ;
};

template <
  template <typename...> class T,
  typename Arg,
  typename... Args
>
struct type_extend_impl<0,T,Arg,Args...>
{
  // Base case: Stop the recursion and expose Arg.
  typedef T<Arg,Args...> type;
};

Certainly do take time to digest the above, but, the above code works since each recursive call to type_extend_impl except the last adds one Arg to the template argument list and reduces N by one.


Testing type_extend

All that is left is to test that this works. This test will assume the use of one of the GNU C++ version 4.5 or higher compilers. Suitable test code would be:

#include <tuple>

// Place definition of type_extend_impl here.
// Place definition of type_extend here.

#include <iostream>
#include <typeinfo>
#include <string>
#include <cstdlib>

#include <cxxabi.h>   // GCC-specific

template <typename T>
std::string demangled_name_of_type()
{
  // NOTE: See G++ Manual Chapter 27.
  //       http://gcc.gnu.org/onlinedocs/libstdc++/
  int status;
  char *name = abi::__cxa_demangle(typeid(T).name(), 0, 0, &status);
  std::string realname(name);
  free(name);
  return realname;
}

int main()
{
  typedef type_extend<0,std::tuple,unsigned>::type test0;
  typedef type_extend<1,std::tuple,unsigned>::type test1;
  typedef type_extend<2,std::tuple,unsigned>::type test2;
  typedef type_extend<3,std::tuple,unsigned>::type test3;
  typedef type_extend<4,std::tuple,unsigned>::type test4;

  std::cout
    << "0: " << demangled_name_of_type<test0>() << std::endl
    << "1: " << demangled_name_of_type<test1>() << std::endl
    << "2: " << demangled_name_of_type<test2>() << std::endl
    << "3: " << demangled_name_of_type<test3>() << std::endl
    << "4: " << demangled_name_of_type<test4>() << std::endl
  ;
}

Save it in a file (e.g., called program.cxx and compile it:

g++ -std=c++0x program.cxx

When it is run, the following output will appear:

0: std::tuple<>
1: std::tuple<unsigned int>
2: std::tuple<unsigned int, unsigned int>
3: std::tuple<unsigned int, unsigned int, unsigned int>
4: std::tuple<unsigned int, unsigned int, unsigned int, unsigned int>

Success!


Template Aliases

Another feature, not yet implemented by GCC, in the next standard is the ability to alias template types:

template <unsigned N, typename Arg>
using n_tuple = typename type_extend<N,std::tuple,Arg>::type;

Using template aliases would allow one to easily eliminate the need to write ::type and to partially substitute template arguments. For example, the template alias n_tuple could subsequently be used as follows:

n_tuple<3,std::string>

which would be equivalent to the t3 typedef at the very top of this file in the Overview section.

One Reply to “A Template Argument Extender”

Leave a Reply

Your email address will not be published. Required fields are marked *