C++ Template Basics
1. Types of Template Parameters: Type vs. Non-Type
C++ templates allow two main kinds of parameters:
| Type | Keyword | What is passed | Example Usage |
|---|---|---|---|
| Type Parameter | typename or class |
Any data type (int, std::string, custom classes, etc.) |
template <typename T> |
| Non-Type Parameter | Basic types (int, bool, size_t, etc.) |
A value (constant) known at compile time | template <int N> |
2. Various Ways to Declare Template Parameters
When declaring template parameters, five main patterns emerge depending on the presence of an Identifier (Name) and a Default Value, each serving a different purpose.
2.1. Non-Type Template Parameters
These parameters pass a value known at compile time to the template (e.g., array sizes, constant flags, etc.).
template <int N>
class A { int v{N}; }; // N is used internally
template <int N = 10>
class B { int v{N}; }; // N is used internally, default value provided
template <int = 10>
class C { int v{0}; }; // Nameless, only default value provided (used for SFINAE)
template <int>
class D { int v{0}; }; // Neither name nor default value (used to satisfy type layout)
template <int, int>
class E { int v{0}; }; // List of multiple parameters
int main()
{
A<10> a{};
B<> b{};
C<> c{};
D<10> d{};
E<10, 10> e{};
}
| Class | Declaration (template <…>) | Characteristics and Role | Example Usage |
|---|---|---|---|
| A | template <int N> |
Named, no default value. The most common form; the passed value is used in internal logic. | Passing a size like 5 in std::array<int, 5> |
| B | template <int N = 10> |
Named, default value provided. Enhances convenience by using a default if no argument is passed. | Templates with optional flags |
| C | template <int = 10> |
Nameless, default value provided. The passed value is not used, and a default value is provided to allow omitting the argument. Used as a logical control gate for std::enable_if. |
Function overloading using std::enable_if |
| D | template <int> |
Nameless, no default value. The passed value is not used, indicating only that one parameter is formally required. | Rarely used (A is clearer) |
2.2. Type Template Parameters
These parameters pass a type to the template (e.g., int, float, custom structs, etc.).
template <typename T>
class A { T v{0}; }; // T is used internally
template <typename T = int>
class B { T v{0}; }; // T is used internally, default value provided
template <typename = int>
class C { int v{0}; }; // Nameless, only default value provided (used for SFINAE)
template <typename>
class D { int v{0}; }; // Neither name nor default value (used to satisfy type layout)
template <typename, typename>
class E { int v{0}; }; // List of multiple parameters
int main()
{
A<int> a{};
B<> b{};
C<> c{};
D<int> d{};
E<int, int> e{};
}
| Class | Declaration (template <…>) | Characteristics and Role | Example Usage |
|---|---|---|---|
| A | template <typename T> |
Named, no default value. The most common form; the passed type is used internally. | Defining the type of a container, like int in std::vector<int> |
| B | template <typename T = int> |
Named, default value provided. Enhances convenience by using a default type (int) if no argument is passed. |
Default comparator in std::map (Compare = std::less<Key>) |
| C | template <typename = int> |
Nameless, default value provided. The passed type is not used, and a default is provided to allow omitting the argument. A core pattern for std::enable_if. |
Function overloading using std::enable_if |
| D | template <typename> |
Nameless, no default value. Indicates only that one type parameter is formally required. | Rarely used |
3. Core Pattern: std::enable_if and Nameless Parameters (Class C)
std::enable_if is inserted into an additional parameter position of a template to exclude the template from overload candidates based on a condition (SFINAE).
3.1. Pattern 1: Using Type Template Parameters
This approach declares an additional type parameter and sets its default value to the result of std::enable_if_t.
#include <iostream>
#include <type_traits> // std::is_integral, std::enable_if_t
// This function is only valid when T is an integral type
template <typename T,
typename = std::enable_if_t<std::is_integral<T>::value, float> // (A)
>
void check_type_foo()
{
std::cout << "Pattern 1: T is an integral type (using Type Parameter)" << std::endl;
}
// Example Usage
int main() {
check_type_foo<int>(); // (A) is substituted with float, success
// check_type_foo<float>(); // (A) substitution fails (SFINAE), compile error
}
How it works:
- On Success (T = int):
std::enable_if_t<true, float>$\to$ substituted withfloattype.- The final template declaration becomes
template <typename T, typename = float>, completing a valid Class C type pattern.
- The final template declaration becomes
- On Failure (T = float):
std::enable_if_t<false, float>$\to$ substitution fails because thetypemember is not found. This function is removed from the compiler’s candidate list.
3.2. Pattern 2: Using Non-Type Template Parameters
This approach declares an additional non-type parameter, sets its type to the result of std::enable_if_t, and assigns a default value (= true).
#include <iostream>
#include <type_traits> // std::is_integral, std::enable_if_t
// This function is only valid when T is an integral type
template <typename T,
std::enable_if_t<std::is_integral<T>::value, bool> = true // (B)
>
void check_type_bar()
{
std::cout << "Pattern 2: T is an integral type (using Non-Type Parameter)" << std::endl;
}
// Example Usage
int main() {
check_type_bar<long>(); // (B) is substituted with bool = true, success
// check_type_bar<double>(); // (B) substitution fails (SFINAE), compile error
}
How it works:
- On Success (T = long):
std::enable_if_t<true, bool>$\to$ substituted withbooltype.- The final template declaration becomes
template <typename T, bool = true>, completing a valid Class C non-type pattern.
- The final template declaration becomes
- On Failure (T = double):
std::enable_if_t<false, bool>$\to$ substitution fails because thetypemember is not found. This function is removed from the candidate list.
Differences and Advantages of Both Patterns
| Feature | Pattern 1: Type Parameter (typename = …) | Pattern 2: Non-Type Parameter (Type = Value) |
|---|---|---|
| Declaration Form | Manipulates the default value of the type parameter. | Manipulates the type of the non-type parameter. |
| Multiple Overloads | Regarded as identical signatures, resulting in compilation errors if you try to define two functions foo with different enable_if conditions. |
Can have different non-type parameter default values, making it suitable for template overloading. |
| Readability | Simple | The Type = Value form is slightly more complex. |
Reference
https://leimao.github.io/blog/CPP-Enable-If/
Leave a comment