C++ Template Basics

5 minute read

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 with float type.
    • The final template declaration becomes template <typename T, typename = float>, completing a valid Class C type pattern.
  • On Failure (T = float): std::enable_if_t<false, float> $\to$ substitution fails because the type member 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 with bool type.
    • The final template declaration becomes template <typename T, bool = true>, completing a valid Class C non-type pattern.
  • On Failure (T = double): std::enable_if_t<false, bool> $\to$ substitution fails because the type member 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/

Tags:

Categories:

Updated:

Leave a comment