Templates are a feature offered by C++ which facilitates generic programming and
provide developers with the ability to parameterize the ‘type’ of argument. For
this blog post I am assuming that the reader has some familiarity with generic programming
and templates. Template
template parameters are one aspect of template offerings by C++ that often gets
overlooked. Though I was aware of this feature in C++ but I did not actually
use it until recently when I happen to study it more meticulously.
The C++ Standard Template
Library (STL) is one of the best examples of usage of templates with
generic containers and algorithms. It provides three sequence
containers namely vector, deque and list. Each of these containers is a template class. The vector class looks like following:
template <
typename Type,
typename Allocator = allocator<Type>
>
class vector
We can instantiate vector class with built-in types
as template argument for e.g.
vector<int> vec1;
vector<string>
vec2;
We can also use instantiated class templates as template
argument for e.g.
vector<list<int>> vec3;
vector<vector<char>> vec4;
In the above example list<int> and vector<char>
are instantiated class templates and not the class template itself. C++ does
allow the template parameters to be class templates themselves. However, we
need to tell the compiler that the template parameter is a class template. Let
us take a look at an example of template template parameter:
template<typename T>
class my_array
{
T* m_data;
int m_size;
public:
void push_back(T value);
void pop_back();
T* begin();
T* end();
};
//
class with template template parameter
template<typename T, template<typename U> class seq>
class container
{
private:
seq<T> m_seq;
public:
void append(const T& value) { m_seq.push_back(); }
T* begin() { return m_seq.begin(); }
T* end() { return m_seq.end(); }
};
container<int, my_array> cont;
In the above example, ‘seq’ is a template template
parameter. There are couple of point to be noted here:
- The template template parameter, ‘seq’ in above example, must be introduced with the keyword class. The keyword typename is not allowed. This is due to the fact that typename indicate a simple type only whereas a template template parameter must be a template class and not a simple type.
- The identifier ‘U’ in the declaration of template template parameter ‘seq’ is only valid inside its own declaration and cannot be used inside the scope of class ‘container’.
We can also write the class ‘container’ in
above example in following manner and achieve exactly same functionality:
template< typename T, typename seq>
class container
{
seq m_seq;
public:
void append(const T& value) { m_seq.push_back(); }
T* begin() { return m_seq.begin(); }
T* end() { return m_seq.end(); }
};
container<int, my_array<int>> cont;
Now you might be having a question in your mind
that if we can achieve the same effect using instantiated class templates, what
is the need for template template parameter or what additional advantage it can
provide? Templates enable us to work with generic types. In other words, it
enables us to abstract the types of the application's local data in the implementation. Template template parameters add to that by enabling one to
introduce an additional level of abstraction. To understand this, let us work
with another example.
Suppose we want to implement a library which
performs numeric operations on very long integers which cannot be represented
by built-in types on operating system. The core of the problem is how to
represent such long integers. There are many options that we can consider using
for e.g. array, singly linked list, doubly linked list, dynamically allocated
memory etc. Given that at the beginning of project we do not have an exhaustive
list of all algorithms that we will be implementing, the possible context under
which algorithms may be used and the fact that choosing a particular data
structure/container for representing the long integer will influence the way we
implement the algorithm; it is hard to choose a perfect container for all
possible scenarios. A wiser approach would be to leave this decision open and
parameterize the long integer class by the ‘container’ which holds the digits.
We keep the interface minimal where a long integer is a sequence of digits and
each digit is accessible by iterator.
Following is a sample implementation of long
integer class using simple template parameters. The class is instantiated by
using the ‘instantiated class template’ of the container as argument for
template parameter.
template<typename cont_type = vector<int>>
class integer_long {
public:
typedef typename cont_type::value_type value_type;
typedef typename cont_type::value_type value_type;
typedef typename cont_type::iterator iterator;
iterator begin() { return cont.begin(); }
iterator end() { return cont.end(); }
void push_back(value_type v) { cont.push_back(v); }
private:
cont_type cont;
};
template<typename cont_type = vector<int>>
integer_long<cont_type> Add(
integer_long<cont_type> num_1,
integer_long<cont_type> num_2
);
//
using default value for template argument
integer_long<> n_1, n_2, n_3;
n_3
= Add(n_1, n_2);
//
using instantiated class template
//
(deque<int>) as template argument
integer_long <deque<int>> n_4, n_5, n_6;
n_6 =
Add(n_4, n_5);
A sample implementation of the long integer class using
template template parameter would look like following. The class is
instantiated using the real class template of the container as argument for
template parameter.
template<template<typename T, typename A> class cont_type = vector>
class integer_long {
public:
typedef int digit_type;
typedef allocator alloc_type
typedef typename
cont_type<digit_type, alloc_type>::iterator iterator;
iterator begin() { return cont.begin(); }
iterator end() { return cont.end(); }
void push_back(digit_type v) { cont.push_back(v); }
private:
cont_type<digit_type, alloc_type> cont;
};
template<
template<typename T, typename A> cont_type = vector,
template<typename U> alloc_type = allocator
>
integer_long<cont_type, alloc_type> Add(
integer_long<cont_type, alloc_type> num_1,
integer_long<cont_type, alloc_type> num_2
);
//
using default value for template argument
integer_long<> n_1, n_2, n_3;
n_3
= Add(n_1, n_2);
//
using real class template (deque and allocator)
//
not the instantiation as template argument
integer_long<deque, allocator> n_4, n_5, n_6;
n_6
= Add(n_4, n_5);
Let us now compare and contrast the two techniques. If
the user chooses not to provide any template arguments and go ahead with
default arguments, both the approaches are same from usage perspective and can
have same underlying implementation ( i.e. sequence container in above example).
Each of the sequence container provided by STL (vector,
deque
and list)
has specific characteristics which are suitable to different context and requirements.
Hence we would want to allow the user to be able to specify the sequence
container of his/her choice. In case of
simple template parameter technique, the user can pass an instantiated class
template (of sequence container of his/her choice) as template argument. Along with type of sequence container, user also provides the internal type
(which is the type of a digit) and memory allocator type, which are used to
instantiate the sequence container. However, more often than not, we might want to treat
these as implementation details. But with simple template parameter technique
we cannot achieve this. This is where template template parameter comes in
handy.
If you look at the sample code above using template
template parameter technique, the type of a digit (i.e. internal type of
sequence container) and type of memory allocator, are treated as implementation
details. We only make them available by defining public types called ‘digit
type’ and ‘alloc_type’ in our long integer class. The user still has the option to specify the type of sequence container of his choice while
instantiating the long integer class.
Also note that template template parameter
approach provides the ability to create multiple, different instantiations of
sequence container inside the class template body using the template template
argument. This is not possible with simple template parameter technique. The additional
level of abstraction and flexibility offered by the template template parameter
technique can be used by library designers and developers to provide specialized
versions of certain algorithms which make use of the specific features of the
underlying container. It gives the users enough flexibility to exercise their
choice depending on context of use meanwhile insulating them from unnecessary implementation specific details.
I hope after reading this post you now have a
better insight and understanding of template template parameter feature offered
by C++.