Template Template Parameters in C++: When Your Template Parameter Is Itself a Template
In C++, template parameters are typically types (typename T) or values (size_t N). But there is a third kind: a template template parameter, where the parameter is itself a template. This is a parameter that is substituted not with the name of a class, but with the name of an entire template (Pikus, 2023). In other words, instead of passing a concrete type like int or std::vector<int>, you pass an uninstantiated template like std::vector (without the angle brackets specifying what it contains).
This post covers the syntax, motivation, and practical considerations of template template parameters, drawing from Chapter 2 of Pikus’s Hands-On Design Patterns with C++ (Pikus, 2023).
Table of Contents
- The Problem: Parameterizing Over Containers
- Syntax of Template Template Parameters
- An Example: The Builder Class
- Template Template Parameters in Function Templates
- The Arity Matching Problem
- The Variadic Solution
- Summary
The Problem: Parameterizing Over Containers
Consider a class that builds up a collection of elements. You want this class to be generic over both the element type and the container type. A first attempt might look like this:
template <typename T, typename Container>
class Builder {
Container data_;
public:
void add(const T& t) { data_.push_back(t); }
};
This works, but it forces the user to specify the full conainer type including its element type:
Builder<int, std::vector<int>> b;
Notice the redundancy: int appears twice. The element type T and the container’s element type must match, but the compiler cannot enforce this. If someone writes Builder<int, std::vector<double>>, the code may compile but behave unexpectedly.
What we want is to pass the container as a template, not as a fully instantiated type. Then the Builder class itself can instantiate the container with the correct element type. This is exactly what template template parameters provide.
Syntax of Template Template Parameters
A template template parameter is declared by specifying that the parameter itself takes template parameters. The general form is:
template <typename T, template <typename> typename Container>
class Builder { /* ... */ };
typename Tis an ordinary type parameter (the element type)template <typename> typename Containerdeclares thatContaineris a template that takes exactly one type parameter
The inner template <typename> specifies the signature of the template we expect. In this case, we expect a template that takes one type argument. The outer typename Container (or equivalently class Container) gives this template template parameter a name.
When you instantiate Builder, you pass an uninstantiated template name:
Builder<int, my_vector> b; // my_vector is a template, not a type
Inside Builder, you can then instantiate the container with whatever type you need:
Container<T> data_; // becomes my_vector<int>
An Example: The Builder Class
Here is a complete example from the book (Pikus, 2023):
template <typename T, template <typename> typename Container>
class Builder {
Container<T> data_;
public:
void add(const T& t) { data_.push_back(t); }
void print() const {
for (const auto& x : data_) std::cout << x << " ";
std::cout << std::endl;
}
};
This Builder template has two parameters:
T: the element typeContainer: a template that takes one type parameter
Inside the class, Container<T> instantiates the container template with the element type. This guarantees type consistency, i.e.: the container will always hold elements of type T.
To use it, you need a container template that takes exactly one type parameter:
template <typename T>
class my_vector {
// implementation...
public:
void push_back(const T&);
// ...
};
Builder<int, my_vector> b;
b.add(1);
b.add(2);
b.print(); // outputs: 1 2
Note that we pass my_vector, not my_vector<int>. The Builder class handles the instantiation internally.
Template Template Parameters in Function Templates
Template template parameters work in function templates too. Consider a function that copies elements from one container type to another (Pikus, 2023):
template <template <typename> class Out_container,
template <typename> class In_container,
typename T>
Out_container<T> resequence(const In_container<T>& in_container) {
Out_container<T> out_container;
for (auto x : in_container) {
out_container.push_back(x);
}
return out_container;
}
This function has three template parameters:
Out_container: a template template parameter for the output containerIn_container: a template template parameter for the input containerT: the element type
The compiler can deduce In_container and T from the function argument. Only Out_container must be explicitly specified:
my_vector<int> v{1, 2, 3, 4, 5};
template <typename T> class my_deque { /* ... */ };
auto d = resequence<my_deque>(v); // my_deque<int> with elements 1, 2, 3, 4, 5
The compiler deduces that In_container is my_vector and T is int from the argument v. We only need to specify Out_container as my_deque because it cannot be deduced from the function parameters.
The Arity1 Matching Problem
An important thing to note is the following: the number of template parameters specified in the template template parameter declaration must match the number of template parameters of the argument template (Pikus, 2023).
Consider this function:
template <template <typename> class Container, typename T>
void print(const Container<T>& container) {
for (auto x : container) std::cout << x << " ";
std::cout << std::endl;
}
std::vector<int> v{1, 2, 3, 4, 5};
print(v); // Does this compile?
The answer depends on your compiler and the C++ standard version. The issue is that std::vector actually has two template parameters, not one (cppreference.com, n.d.):
template <typename T, typename Allocator = std::allocator<T>>
class vector;
The second parameter (the allocator) has a default value, which is why you can write std::vector<int> without specifying the allocator (Pikus, 2023). But as far as the template signature is concerned, std::vector takes two type parameters, not one.
So when we declare template <typename> class Container, we are saying “give me a template that takes exactly one type parameter”. But std::vector takes two, causing a mismatch.
Different compilers handle this differently. Some relax the requirement and allow templates with default parameters to match. GCC, Clang, and MSVC all relax this requirement to some degree, but not to the same degree (Pikus, 2023). This means code that compiles on one compiler may fail on another. Clearly this is not a portable solution.
The Variadic Solution
Variadic templates offer a more robust solution, at least in C++17 and later (Pikus, 2023). Instead of specifying a fixed number of type parameters, we specify that the template can take any number of type parameters:
template <template <typename...> class Container, typename... T>
void print(const Container<T...>& container) {
for (auto x : container) std::cout << x << " ";
std::cout << std::endl;
}
The typename... inside the template template parameter declaration means “any number of type parameters”. This matches std::vector (two parameters: element type and allocator), std::set (three parameters: element type, comparator, and allocator), or any other standard container (cppreference.com, n.d.).
Similarly, our Builder class can be made more flexible:
template <typename T, template <typename...> typename Container>
class Builder {
Container<T> data_;
public:
void add(const T& t) { data_.push_back(t); }
void print() const {
for (const auto& x : data_) std::cout << x << " ";
std::cout << std::endl;
}
};
// Now works with std::vector, std::deque, std::list, etc.
Builder<int, std::vector> b;
b.add(1);
b.add(2);
b.print();
When we instantiate Container<T> inside the class, the container’s other template parameters (like the allocator) take their default values. This is the idiomatic way to write template template parameters in modern C++.
Summary
Template template parameters allow you to parameterize a template over another template, rather than over a concrete type. Some takeways from this post are the following:
- Syntax:
template <typename> typename Containerdeclares a template template parameter that expects a template taking one type argument - Instantiation: Inside the template, you instantiate the parameter like any other template:
Container<T> - Arity matters: The number of template parameters in the declaration must match the argument template, but this is relaxed by most compilers for templates with default arguments
- Variadic solution: Use
template <typename...> typename Containerfor maximum compatibility with standard library containers
Template template parameters are particularly useful when you want to defer the choice of container type while ensuring type consistency between the container and its elements. Rather than passing std::vector<int> and hoping the user got the element type right, you pass std::vector and let the template instantiate it correctly.
Finally, I really recommend reading Hands-On Design Patterns with C++ (Pikus, 2023) if you want to go deeper on this and related topics. People say programming is something you need to do hands-on, reading books is useless. Of course I agree on the former statement, but not the latter. Reading books can speed up knowledge acquisition, for example: something you’d only learn after 3 years in the industry, you may acquire that something in, say, just 1 year because you’ve learned it from a book. However, this is conditioned on NOT just reading the book, but doing things as well as you read, type the code in your editor, compile it, break things, etc., at least this is how it works for me in terms of retaining what I read; or write a blog post like this ^_^ (coding still more fun than blog-writing though, imo)
References
cppreference.com. (n.d.). std::vector. https://en.cppreference.com/w/cpp/container/vector
cppreference.com. (n.d.). std::set. https://en.cppreference.com/w/cpp/container/set
Pikus, F. G. (2023). Hands-On Design Patterns with C++ (Second Edition). Packt Publishing.
-
Arity refers to the number of arguments or operands a function or operation takes. A unary function has arity 1, a binary function has arity 2, and so on. In the context of templates, arity refers to how many template parameters a template accepts. See Arity (Wikipedia). ↩︎