Template Template Parameters in C++ Class Templates
In C++, declaring a template template parameter inside a class template enables higher-order generic programming by allowing the class to accept a template itself rather than a fully instantiated type. This article explores the mechanics and effects of this feature, demonstrating how it decouples container templates from their contained types, enforces compile-time constraints, and enhances code flexibility and expressiveness.
Understanding the Concept
A standard template parameter typically accepts a concrete type (such
as int or std::string) or a non-type value. In
contrast, a template template parameter accepts an uninstantiated
template. By declaring one inside a class template, you shift the
responsibility of type instantiation from the caller to the class
template itself.
Consider the basic declaration syntax:
template <template <typename...> class Container, typename T>
class DataHandler {
Container<T> primary_storage;
Container<int> id_storage;
};In this example, Container is a template template
parameter. It is not a complete type until DataHandler
instantiates it internally with specific arguments such as
T or int.
Core Effects on Class Design
1. Decoupling Templates from Element Types
Without template template parameters, passing a container type like
std::vector<int> requires specifying both the
container and its element type at the call site. If the enclosing class
needs to use that same container structure for other types, it cannot do
so easily.
Declaring a template template parameter allows the enclosing class
template to supply or vary the inner type. The user only provides the
blueprint of the container (e.g., std::vector), and the
class template decides what types it will store.
2. Multiple Instantiations of the Same Family
Because the template parameter remains uninstantiated until used
inside the class body, the class can instantiate the provided template
multiple times with different type arguments. A single class template
can instantiate Container<T>,
Container<std::string>, and
Container<size_t>, ensuring structural consistency
across disparate data types.
3. Strict Compile-Time Signature Matching
Declaring a template template parameter imposes strict matching rules. The compiler verifies that the argument passed by the caller matches the template parameter list of the template template parameter.
For instance, standard library containers like
std::vector typically take an allocator parameter with a
default argument:
template <typename T, typename Allocator = std::allocator<T>>
class vector;Prior to C++17, a template template parameter declared as
template <typename> class Container would fail to
match std::vector because the allocator parameter was not
accounted for. Using variadic template template parameters
(template <typename...> class Container) solves this
by accepting templates with any number of parameters, making the design
robust and backward-compatible.
4. Type Safety and Error Localization
When a class template expects a template template parameter, passing
a non-template type (such as int or an already instantiated
std::vector<int>) results in an immediate
compile-time error. This prevents invalid type substitutions early in
the compilation process and produces clearer diagnostics compared to
errors deep within nested type definitions.