C++ Templates: Class Templates, Aliases, and Template Template Parameters


C++ templates are one of the language's most powerful features, giving developers the tools to write flexible, reusable, and highly generic code. But as you dive deeper into the world of templates—beyond the basics and into areas like template aliases and template template parameters—you may encounter behaviors that are anything but intuitive. In fact, what works perfectly in one compiler might behave differently in another, leaving you scratching your head.
One particularly tricky situation comes up when trying to check if a type is a specialization of a given template—only to run into trouble when a template alias is involved.
In this article, we’ll break down these advanced template features in a clear and approachable way. We'll look at the differences between class templates and template aliases, and explore why template aliases can’t be used as template template parameters. By the end, you’ll not only understand how these features work, but also why compilers sometimes seem to disagree on what’s valid code.
1. Understanding Class Templates
Templates allow you to write generic code that works with multiple data types. There are two main kinds of templates:
Function Templates – Define functions that operate on different types.
Class Templates – Define classes that can handle various data types.
Example: Defining a Class Template
A class template is a blueprint for creating classes with different types:
template <typename DataType>
struct GenericContainer {
DataType value;
};
GenericContainer<int>
is an instantiation ofGenericContainer
forint
.GenericContainer<double>
operates withdouble
.
How the Compiler Expands Templates
When we declare:
GenericContainer<int> obj;
The compiler generates a specific version of GenericContainer<T>
for int
:
struct GenericContainer<int> {
int value;
};
Similarly, for GenericContainer<double>
:
struct GenericContainer<double> {
double value;
};
This process is called template instantiation, where the compiler creates a unique version of the template for each used type.
2. Understanding Template Aliases
A template alias introduces an alternative name for an existing template, providing syntactic convenience but not defining a new distinct template.
Example:
template <typename T>
using AliasContainer = GenericContainer<T>;
AliasContainer<int>
is exactly the same asGenericContainer<int>
.However,
AliasContainer
itself is not a real class template, it’s just an alias forGenericContainer<T>
.
How the Compiler Handles Aliases
Aliases are purely syntactic replacements. When you use AliasContainer<int>
, the compiler replaces it with GenericContainer<int>
, making them indistinguishable.
This distinction becomes crucial when dealing with template template parameters, as discussed next.
3. Understanding Template Template Parameters
A template template parameter allows a template to accept another template as an argument. Unlike regular template parameters that take types (e.g., int
or double
), a template template parameter takes a template itself.
Basic Example:
template <template <typename> typename TemplateWrapper>
struct Container {
TemplateWrapper<int> value; // TemplateWrapper must be a template that accepts one typename parameter
};
Here, TemplateWrapper
is expected to be a class template that takes a single type argument, such as:
template <typename T>
struct DataHolder {
T data;
};
Usage:
Container<DataHolder> obj; // Works fine
If we try to pass a template alias instead of a real template:
template <typename T>
using AliasDataHolder = DataHolder<T>;
Container<AliasDataHolder> obj; // Error!
The compiler rejects AliasDataHolder
because it is not a primary class template, just a shortcut.
More Complex Example: Nested Template Template Parameters
Template template parameters can be nested, allowing multiple levels of generic abstraction. This is useful when a higher-level template needs to accept another template, which in turn expects another template as an argument.
Example of Nested Template Template Parameters
template <template <typename> typename OuterTemplate>
struct OuterContainer {
template <template <typename> typename InnerTemplate>
struct InnerContainer {
OuterTemplate<int> outer;
InnerTemplate<int> inner;
};
};
Breakdown of This Code:
OuterContainer
is a class template that accepts another class template (OuterTemplate
) as a parameter.Inside
OuterContainer
, we define another templateInnerContainer
, which itself takes a class template (InnerTemplate
) as a parameter.OuterTemplate<int>
andInnerTemplate<int>
are instantiated withinInnerContainer
.
Usage Example:
template <typename T>
struct WrapperA {
T dataA;
};
template <typename T>
struct WrapperB {
T dataB;
};
using ComplexType = OuterContainer<WrapperA>::InnerContainer<WrapperB>;
ComplexType obj;
Key Insights:
OuterContainer<WrapperA>
acceptsWrapperA
as theOuterTemplate
.InnerContainer<WrapperB>
acceptsWrapperB
as theInnerTemplate
.The resulting
ComplexType
has bothWrapperA<int>
andWrapperB<int>
inside it.
Why Is This Useful?
Allows flexible composition of multiple levels of templates.
Enables highly generic container structures without tightly coupling them.
Avoids code duplication by reusing template parameters at different levels.
However, this approach still requires actual class templates, not template aliases.
Verifying Template Specializations
A practical example is checking whether a type is a specialization of a given template:
template <typename, template <typename...> typename>
constexpr bool is_instance_of_template_v = false;
// Specialization: if the first parameter is an instance of the second template
template <template <typename...> typename BaseTemplate, typename... Args>
constexpr bool is_instance_of_template_v<BaseTemplate<Args...>, BaseTemplate> = true;
This verifies whether a given type is an instance of a given template.
How It Works Internally
The first declaration defaults
is_instance_of_template_v<Type, Template>
tofalse
.The specialization matches cases where the first parameter is an instantiation of the second template.
If
BaseTemplate<Args...>
matchesBaseTemplate
, the result istrue
.
Usage Example:
static_assert(is_instance_of_template_v<GenericContainer<int>, GenericContainer>, "This works");
GenericContainer<int>
is indeed a specialization ofGenericContainer
, so the assertion passes.
Now, let's test:
static_assert(is_instance_of_template_v<AliasContainer<int>, AliasContainer>, "This fails");
What Happens Internally?
AliasContainer<int>
is replaced withGenericContainer<int>
.However,
is_instance_of_template_v<GenericContainer<int>, AliasContainer>
is being checked.AliasContainer
is not a real class template; it's just an alias forGenericContainer
.Since
template <typename...> typename BaseTemplate
expects a real class template,AliasContainer
fails the check.
Compiler Behavior:
GCC allows it (incorrectly, per the standard).
MSVC and Clang reject it (correct behavior according to the C++ standard).
Standard Explanation
According to the C++ standard ([temp.param]), a template template parameter must be a primary class template:
"A template alias is not a distinct template and cannot be partially or explicitly specialized."
Thus, AliasContainer
does not qualify as a valid template for template <typename...> typename BaseTemplate
.
How to Fix the Issue
The correct way to check if AliasContainer<int>
is a specialization of a given template is to refer to GenericContainer
directly:
static_assert(is_instance_of_template_v<AliasContainer<int>, GenericContainer>, "This works");
Alternative Fix: Using a Helper Structure
To make AliasContainer
act like a real template, use an intermediate helper struct:
template <typename T>
struct AliasWrapper {
using type = GenericContainer<T>;
};
// Usage:
template <typename T>
using AliasContainer = typename AliasWrapper<T>::type;
Even though this makes AliasContainer
behave more like a class template, it still won’t work as a template template parameter.
In Summary
Templates are one of the most powerful features of C++, enabling generic programming and reducing code duplication. We started by understanding class templates, which allow us to define blueprints that can be instantiated with different data types. This was followed by template aliases, which provide a way to create shorter or more readable names for existing templates. However, template aliases are merely syntactic sugar—they do not introduce a new type but rather act as direct replacements for an existing template.
Moving forward, we explored template template parameters, which take other templates as arguments. This is where things get interesting: unlike regular template parameters that take specific types (like int
or double
), a template template parameter expects a primary class template—meaning an actual template definition, not just an alias. This distinction is crucial, as it directly affects template specialization and type deduction.
We then encountered a real-world issue: detecting whether a given type is a specialization of a particular template. The approach using is_instance_of_template_v
works fine for class templates like GenericContainer<int>
, but fails when using a template alias such as AliasContainer<int>
. This is because an alias is not a primary template—it is just a name substitution. Internally, the compiler replaces AliasContainer<int>
with GenericContainer<int>
, and the specialization check then fails since the compiler does not see AliasContainer
as a distinct template.
Interestingly, GCC incorrectly allows this alias-based specialization check, while Clang and MSVC correctly reject it based on the C++ standard. According to the standard, template template parameters must be primary templates, and aliases do not qualify.
To address this, we explored possible fixes, such as directly referring to the original template (GenericContainer
instead of AliasContainer
) or using an intermediate helper structure to wrap the alias. However, even with these workarounds, template aliases still cannot be used as template template parameters.
Subscribe to my newsletter
Read articles from Bhanuprakash Eagala directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Bhanuprakash Eagala
Bhanuprakash Eagala
Computer Science enthusiast