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.