In Chapter 2 : Working with Function Templates in C++, we explored the mechanics of function templates and encountered the term template parameter. This chapter builds on that foundation by focusing specifically on template parameters themselves, examining what they represent, how they are classified, and how they are used in practice. The intent is to establish a clear and precise understanding of template parameters before moving on to more advanced aspects of template programming.

In this chapter we will discuss the following topics:

Introduction to Template Parameters

Before examining template parameters in more detail, it is useful to first revisit function parameters, since template parameters build directly on this familiar concept. A function parameter is a mechanism through which values are supplied to a function at the point of a function call. When we define a regular, non-template function, we explicitly specify the function parameters along with their respective data types, and these types are fixed at compile time.

Example 3.1: A simple function with fixed parameter types

int foo(int x, int y)
{
    return x + y;
}

In this example, the function foo is defined with two parameters, x and y, both of type int. When calling this function, only values of compatible types can be provided. The data types of the parameters are part of the function’s definition and cannot vary between calls. In other words, there is no mechanism in a regular function to defer the choice of parameter types.

Template functions address this limitation by allowing the parameter types to be left unspecified until the function is instantiated. This is achieved through a special kind of parameter known as a template parameter. Rather than representing a value, a template parameter represents a type that the compiler substitutes during template instantiation, based on how the template is used.

Consider the following template function:

Example 3.2: A function template using a type parameter

template<typename T>
bool is_equal(T x, T y)
{
    return x == y;
}

Here, T is a template parameter that represents a type. It does not receive a value at runtime, nor is it passed in the same way as a function argument. Instead, the compiler replaces T with an actual type when the template is instantiated. As a result, the same function template can be used with different types, provided the operations inside the function are valid for those types.

There are different kinds of template parameters. In the next section we will discuss those in detail.

Classification of Template Parameters

In the earlier examples, the template parameters we have used are type parameters. Template parameters in C++ are broadly classified based on what they represent and how the compiler interprets them during template instantiation.

Type parameters

A type parameter acts as a placeholder for a generic data type that the compiler replaces with a concrete type during the instantiation process. While the compiler often deduces the actual data type from the arguments passed in a function call, it can also be explicitly defined by the developer to ensure precise control over the generated code.

Non-type parameters

Non-type parameters, by contrast, do not represent data types. Instead, a non-type template parameter represents a value that is fixed at compile time, such as the number of elements in an array or a configuration value that must be known before the program runs. Because these values are resolved during compilation, they enable the creation of highly optimized, fixed-size structures and allow the compiler to perform arithmetic or logic before the program ever runs.

Relationship with Regular Function Parameters

It is essential to remember that template parameters do not replace regular non-template function parameters. Rather, they work in tandem. Template parameters define the structural and type-level properties of the function at compile time. At runtime, standard parameters handle the actual data values. The following sections will first explore the mechanics of type parameters in detail. Then there will be a deep dive into the practical applications of non-type parameters.

Working with type parameters

We begin this section by focusing on type parameters, as they are the most commonly encountered form of template parameter in C++. The following example demonstrates a function template that combines a type parameter with an ordinary, non-template function parameter. This combination allows compile-time type flexibility alongside run-time values.

Example 3.3: Using a Type Parameter with a Regular Function Parameter

📎 dynamic_allocator.cpp

#include <iostream>
#include <vector>
#include <array>  // For NTTP version later

// HAL: Initialize sensor reading buffer
template<typename SensorData>
std::vector<SensorData> init_sensor_buffer(SensorData initial_reading, size_t buffer_size) {
    return std::vector<SensorData>(buffer_size, initial_reading);
}

int main() {
    // Temperature sensor (float)
    auto temp_buffer = init_sensor_buffer(25.0f, 100);
    std::cout << "Temp buffer[0]: " << temp_buffer[0] << "\n";

    // Pressure sensor (int16_t)  
    auto pressure_buffer = init_sensor_buffer(int16_t{1013}, 50);
    std::cout << "Pressure buffer[0]: " << pressure_buffer[0] << "\n";

    // Status flags (bool)
    auto status_buffer = init_sensor_buffer(false, 8);
    std::cout << "Status buffer[0]: " << status_buffer[0] << "\n";

    return 0;  // Auto cleanup
}

In this program, we have implemented a sensor buffer initialization utility using a function template. The function template init_sensor_buffer takes a single template type parameter SensorData and a regular, non-template function parameter buffer_size. In the main() function, init_sensor_buffer is invoked with three different sets of arguments.

Instantiation with a float Type Argument

In this case, we pass the value 25.0f along with the non-template argument 100. From this usage, the compiler deduces the template type parameter SensorData as float. This deduction is based on the type of the first function argument. As a result, the compiler instantiates the function template init_sensor_buffer with SensorData bound to float. Within the function body, a buffer is created for 100 objects of type float, each initialized to 25.0f.

auto temp_buffer = init_sensor_buffer(25.0f, 100);

Instantiation with an int16_t Type Argument

In this case, the value 1013 is passed along with the non-template argument 50. Based on the type of the first function argument, the compiler deduces the template type parameter SensorData as int16_t. Consequently, the compiler instantiates a separate version of the function template init_sensor_buffer with SensorData bound to int16_t. The function then creates a buffer for 50 objects of type int16_t.

auto pressure_buffer = init_sensor_buffer(int16_t{1013}, 50);

Instantiation with a bool Type Argument

In this case, the first argument is the boolean literal false, while the second argument remains the non-template parameter 8. From this usage, the compiler deduces the template type parameter SensorData as bool and instantiates the function template accordingly. A buffer is created for 8 objects of type bool.

auto status_buffer = init_sensor_buffer(false, 8);

As these examples demonstrate, the template type parameter SensorData is deduced by the compiler during template instantiation based on the types of the arguments provided. In contrast, the regular function parameters behave like those in any standard function. Their types are fixed in the function declaration, and they receive their specific data values only when the function is executed at runtime. This distinction is crucial: template parameters define the structure and logic of the code at compile time, while regular parameters handle the actual data that flows through that logic during program execution. This allows both to coexist within the same function template, each serving a distinct and well-defined role.

In the next section, we will examine non-type template parameters and how they differ from type parameters in both purpose and usage.

Working with non-type parameters

Non-type template parameters differ fundamentally from type parameters. While type parameters act as placeholders for types, non-type template parameters represent values that are fixed at compile time. These values must be constant expressions. The compiler can fully evaluate them during compilation or, in some cases, during the linking phase.

Commonly used categories of non-type template parameters include:

  • Enumeration values
  • Integral values
  • Pointers to objects with external linkage
  • Pointers to functions
  • The null pointer constant

In the following section, we will look at the non-type template parameter of the enum type.

Enums as Non-Type Template Parameters

When declaring a non-type template parameter, an enumeration type may be used. The following example demonstrates this usage.

Example 3.4: Using an enumeration as a non-type template parameter

📎 non_type_enum.cpp

#include <iostream>
enum myenum {
    ONE = 1,
    TWO = 2,
    THREE =3
};
template <myenum val> //pointer to object
void f()
{
   std::cout << val << std::endl;
}
int main()
{
    f<ONE>();
    return 0;
}

In this example, we define an enumeration myenum and a function template f that takes a non-type template parameter val of enumeration type. When calling the function template, only values defined within the enumeration can be supplied as template arguments. This restriction is enforced at compile time. In the next section, we examine integral values as non-type template parameters.

Integer as non-type template parameter

Integral values are among the most commonly used forms of non-type template parameters. To illustrate this, we revisit the sensor buffer example and modify it to accept the buffer size as a non-type template parameter.

Example 3.5: Using an Integral Non-Type Template Parameter

📎 init_fixed_sensor_buffer.cpp

#include <iostream>
#include <array>

// HAL: Fixed-size sensor buffer (compile-time size)
template<size_t BufferSize, typename SensorData>
std::array<SensorData, BufferSize> init_fixed_sensor_buffer(SensorData initial_reading) {
    std::cout << "BufferSize = " << BufferSize << std::endl;
    return std::array<SensorData, BufferSize>{initial_reading};
}

int main() {
    constexpr size_t len = 10;
    auto temp_buffer = init_fixed_sensor_buffer<len>(25.0f);  // float, N=10
    for (auto val : temp_buffer) std::cout << val << " ";
    std::cout << "\n";

    auto pressure_buffer = init_fixed_sensor_buffer<50>(int16_t{1013});  // int16_t, N=50
    for (auto val : pressure_buffer) std::cout << val << " ";
    std::cout << "\n";

    constexpr size_t len3 = 8;
    auto status_buffer = init_fixed_sensor_buffer<len3>(false);  // bool, N=8
    for (auto val : status_buffer) std::cout << val << " ";
    std::cout << "\n";

    // No manual cleanup - std::array RAII
    return 0;
}

Notice that in the earlier implementation of the sensor buffer, BufferSize was a regular function parameter. In this version, BufferSize is declared as a non-type template parameter, as shown below:

template<size_t BufferSize, typename SensorData>

The type of BufferSize is size_t. Because non-type template parameters must be constant expressions, the value substituted for BufferSize must be known at compile time. We now examine how such a parameter is supplied when calling the template.

constexpr size_t len = 10;
auto temp_buffer = init_fixed_sensor_buffer<len>(25.0f);

Here, len is declared as a constexpr size_t and initialised with the value 10. It is then passed as a non-type template argument using angle brackets. The constexpr qualifier is essential, because only compile-time constants are permitted as non-type template arguments. Without it, len becomes a run-time variable whose value is not usable during template instantiation.

A variable whose value can change at run time cannot be used as a non-type template argument. To demonstrate this restriction, remove the constexpr qualifier and attempt to pass the variable as shown below:

size_t len = 10;
auto temp_buffer = init_fixed_sensor_buffer<len>(25.0f);

If you now try to compile the program, compilation fails with the following error:

error: the value of 'len' is not usable in a constant expression
note: 'size_t len' is not const
    size_t len = 10;

As expected, the compiler reports that len is not a constant expression. Another important detail illustrated by this example is the order of template parameters. The template is declared as:

template<size_t BufferSize, typename SensorData>

Accordingly, the non-type template argument must appear before the type argument when supplying template arguments. From the main function, the template is invoked as follows:

init_fixed_sensor_buffer<len>(25.0f)

Here, the constant len substitutes the non-type parameter BufferSize, while the value 25.0f is passed as a regular function argument. Now consider reversing the order of the template parameters:

template<typename SensorData, size_t BufferSize>

With this declaration, the following call will no longer compile:

auto temp_buffer = init_fixed_sensor_buffer<len>(25.0f);

In this case, the order of template arguments in the call no longer matches the template parameter list. To make the call valid, both the type argument and the non-type argument must be specified explicitly:

auto temp_buffer = init_fixed_sensor_buffer<float, len>(25.0f);
auto pressure_buffer = init_fixed_sensor_buffer<int16_t, 50>(int16_t{1013});
auto status_buffer = init_fixed_sensor_buffer<bool, len3>(false);

This requirement follows directly from the rules governing template parameter ordering and substitution. In the next section, we will examine how pointer values can be used as non-type template parameters and the additional constraints that apply to them.

Memory Identities as Non-Type Parameters 

While utilizing integers or booleans as non-type parameters is common, C++ templates also support the use of pointers and references. This capability allows developers to bind templates directly to specific objects or memory locations. However, the compiler is strictly selective regarding which addresses qualify as valid template arguments. Under the ISO/IEC 14882 C++ Standard, any address used as a template argument must be a constant expression. It must have a fixed, permanent identity in memory that the linker can resolve. This requirement necessitates the use of objects with static storage duration and external linkage.

The Advantage: Templates versus Regular Functions

A central architectural question is important to address. Why use a template parameter when a regular function could simply accept a const pointer? The distinction lies in when the address is resolved and how the compiler treats that information.

In a standard function, a pointer is a runtime variable. Even if marked const, the compiler must generate machine code to fetch that address from memory or a register and dereference it during execution. By contrast, a non-type template parameter binds the address during the compilation and linking phases. Because the address is fixed at the point of instantiation, the compiler treats it as a literal constant. This enables direct addressing, effectively baking the physical memory location into the machine code. This eliminates pointer management overhead and allows for aggressive inlining, adhering to the zero overhead principle fundamental to high performance C++.

The Variable Pointer Pitfall

A frequent technical hurdle is attempting to pass a pointer variable to a template. A pointer variable, by definition, is a mutable container. The value stored inside that pointer, which is the address of the target data, is not a link time constant. The address it holds can be modified during execution. Consequently, the compiler cannot use the contents of a variable to generate a stable, compile time template instance.

Example 3.6: Invalid Use of a Pointer Variable

📎 non_type_ptr.cpp

#include <iostream>
#include <cstring>
#include <cstdlib>

// ptr is a variable; the address it holds can change at runtime
char* ptr;

template<char* str>
void print_str()
{
    // In a valid template, str would be a constant address
    std::cout << str << "\n";
}

int main()
{
    // The address held by ptr is determined at runtime via malloc
    ptr = static_cast<char*>(std::malloc(32));
    if (ptr) {
        std::strcpy(ptr, "Hello World");
        
        // Compilation Error: 'ptr' is a variable, not a constant address
        // print_str<ptr>(); 
        
        std::free(ptr);
    }
    return 0;
}

In this example, we have defined a template function print_str. It uses a non-type template parameter str, which is a char type pointer. In the main() function, we have allocated memory by calling the malloc() library function and assigned the address of the allocated memory to a char type pointer variable named ptr.

If you try to compile this program, you will see the following compilation error:

g++ non_type_ptr.cpp -o non_type_ptr -Wall -Wextra -Wpedantic -Werror
non_type_ptr.cpp:24:19: error: ‘ptr’ is not a valid template argument because ‘ptr’ is a variable, not the address of a variable
    print_str<ptr>();

The error message is pretty much self-explanatory. ptr is a pointer-type variable, and its value is determined at run time. Since non-type template arguments must be constant expressions known at compile time, a pointer variable cannot be used as a non-type argument to a template function. The compiler is complaining about exactly this violation.

Pointer to function as non-type template parameter

We can also pass function addresses as template arguments. This technique is a cornerstone of performance-critical software, particularly in systems where every clock cycle counts. By providing a function pointer at compile time, you enable the compiler to perform optimizations that are impossible with traditional runtime callbacks.

In environments like aerospace or defense, where low latency and deterministic behavior are non-negotiable, this pattern replaces the overhead of virtual functions or std::function wrappers with direct, static dispatch.

Example 3.11: Implementing a Zero-Overhead Interrupt Dispatcher

📎 sample_interrupt_dispatch_template.cpp

#include <iostream>

// GPIO ISR handler representing a hardware abstraction layer function
void gpio_interrupt_handler() 
{
    std::cout << "GPIO edge detected: clearing interrupt flag\n";
    // Logic for clearing flags or toggling peripheral states would reside here
}

template<void (*ISR)()>
void dispatch_interrupt() 
{
    // The compiler knows the exact function address at compile time
    ISR(); 
}

int main() 
{
    // The handler is passed as a non-type template parameter
    dispatch_interrupt<gpio_interrupt_handler>();
    return 0;
}

The program begins by defining a free function named gpio_interrupt_handler. This function represents an interrupt service routine, or ISR, typically found in hardware abstraction layers within embedded systems. In real deployments, such a function would contain logic to clear interrupt flags, acknowledge hardware events, or update peripheral state. Here, it simply prints a message to illustrate execution flow.

Next, we define a template function dispatch_interrupt that takes a non-type template parameter ISR, declared as a pointer to a function with no parameters and no return value:

template<void (*ISR)()>
void dispatch_interrupt()
{
    ISR();
}

The key point here is that ISR is not a runtime variable. It is a compile-time constant representing the address of a specific function. Because this address is supplied as a template argument, the compiler knows exactly which function will be called when the template is instantiated.

Inside the function body, the call to ISR() is statically bound. There is no runtime lookup, no indirect call through a function pointer variable, and no virtual dispatch. The compiler can therefore generate direct call instructions and, in some cases, may even inline the function if optimization settings permit. In the main() function, the template is instantiated and invoked as follows:

dispatch_interrupt<gpio_interrupt_handler>();

Here, the address of gpio_interrupt_handler is passed as a non-type template argument. The function has external linkage. It also has a fixed address known at link time. Therefore, it satisfies the requirements for a non-type template parameter.

This pattern is particularly relevant in embedded and real-time systems. In these systems, interrupt dispatch paths must be as short and deterministic as possible. By binding the interrupt handler at compile time, the design avoids the overhead of runtime callbacks while still keeping the dispatch logic generic and reusable.

The example illustrates how non-type template parameters can be used to express compile-time configuration of behaviour, allowing high-performance, low-latency code without sacrificing clarity or type safety.

When you use a function pointer as a non-type template parameter, the compiler does not treat it as a variable stored in memory. Instead, it treats the function address as a literal constant. This allows the compiler to bake the destination address directly into the machine code or, in many cases, inline the call entirely.

This approach provides a significant advantage in static dispatch. It ensures that the function signature is verified during compilation, preventing the mismatched callbacks that often plague traditional C-style systems. Furthermore, by eliminating the need for a vtable lookup or an indirect pointer jump, you achieve a zero-cost abstraction that is perfectly suited for high-frequency interrupt service routines or real-time policy-based drivers.

In the next section, we will explore how to manage multiple template parameters simultaneously to build even more flexible abstractions.

In the next section, we will see how multiple template parameters can be passed to template functions.

Working with multiple type-parameters

While a single template parameter enables basic generic behaviour, real-world software systems often require templates to operate across multiple, distinct data types simultaneously. Supporting multiple type parameters allows interfaces to model interactions between heterogeneous types while preserving strict compile-time type safety. This capability is essential for operations where inputs originate from different domains, such as comparing a sensor’s raw integer output against a floating-point calibration threshold or associating a numeric error code with a descriptive string message.

By employing multiple template parameters, the compiler generates distinct, type-safe implementations for each unique combination of arguments provided at the call site. This removes the need for manual casting and avoids writing and maintaining multiple near-identical function overloads, ensuring that the resulting binary remains compact and computationally efficient. This architectural pattern is widely used in embedded logging systems, configuration managers, and Hardware Abstraction Layer (HAL) factories, where data frequently originates from independent and loosely coupled system layers.

Example 3.13: Processing Heterogeneous Data Types for System Events

📎 sample_heterogeneous_event_processor.cpp

#include <iostream>
#include <string>

/**
 * A generalized event processor handling two distinct types.
 * T1 and T2 are deduced independently, allowing the function to 
 * bridge different data domains without manual type conversion.
 */
template <typename T1, typename T2>
void process_system_event(const T1& descriptor, const T2& data)
{
    // The compiler ensures both types support the stream insertion operator.
    // Each unique combination of T1 and T2 results in a specialized instantiation.
    std::cout << "[EVENT: " << descriptor << "] Data: " << data << "\n";
}

int main()
{
    // Case 1: Mapping a string literal (const char*) to a numeric status code (int)
    process_system_event("SYS_INIT", 200);

    // Case 2: Mapping a numeric ID (int) to a floating point telemetry value (float)
    process_system_event(1042, 36.6f);

    // Case 3: Mapping a character code (char) to a string status message (const char*)
    process_system_event('W', "VOLTAGE_UNSTABLE");

    return 0;
}

In this program, we demonstrate how multiple type template parameters enable a single interface to safely and naturally operate across heterogeneous data domains, without requiring explicit casts or a family of overloaded functions.

The template function process_system_event is declared with two independent type parameters, T1 and T2:

template <typename T1, typename T2>
void process_system_event(const T1& descriptor, const T2& data)

Each template parameter represents a distinct type domain. There is no requirement that T1 and T2 be related in any way. This design allows the function to accept combinations of inputs that originate from different layers of a system, such as identifiers, status codes, telemetry values, or diagnostic messages.

Inside the function body, both parameters are written to standard output using the stream insertion operator. At compile time, the compiler verifies that the specific types bound to T1 and T2 support this operation. If a type does not meet this requirement, compilation fails, ensuring strict type safety.

The Mechanism of Static Specialization

The efficiency of multiple type parameters stems from the underlying compilation process. When the compiler encounters different combinations of types, it does not attempt to force them into a single, generic function at the machine code level. Instead, it fabricates a unique, optimized version of the logic for each specific pair of types encountered in the source code.

In the provided example, the compiler generates three distinct functions within the binary:

  • Version A: Optimized specifically for const char* and int.
  • Version B: Optimized specifically for int and float.
  • Version C: Optimized specifically for char and const char*.

Architectural Advantages and Industry Relevance

In high reliability sectors such as aerospace and defense, this pattern is critical for maintaining data integrity across complex system boundaries. It allows developers to build diagnostic and telemetry layers that remain type aware regardless of the underlying data format.

  • Decoupled Interfaces: Logic can connect two different subsystems without requiring them to share a common class hierarchy or forcing performance heavy runtime conversions.
  • Zero Overhead Abstraction: There is no “least common denominator” type conversion. An integer remains an integer and a float remains a float, preserving the maximum precision and safety supported by the hardware.
  • Compile Time Validation: If the template logic requires an operation that one of the types does not support, the compiler will issue an error at the call site, preventing potential runtime crashes in the field.

This approach adheres to the Zero Overhead Principle by ensuring that the final binary only contains the code necessary for the types actually utilized in the application.

Summary

  • Template parameters are compile-time entities that define how a template is instantiated; they are not runtime values and are resolved during compilation or linking.
  • C++ template parameters are broadly classified into type parameters and non-type parameters, each serving a distinct role in template programming.
  • Type template parameters act as placeholders for concrete data types and allow the compiler to generate type-safe, specialized implementations based on how the template is used.
  • Type parameters are commonly deduced automatically in function templates from function arguments, but they may also be explicitly specified to control instantiation.
  • Non-type template parameters represent values, not types, and must be constant expressions known at compile time or link time.
  • Using integral non-type parameters enables compile-time configuration, such as fixed-size memory allocation, allowing stronger optimization and eliminating runtime checks.
  • Non-type template parameters cannot accept variables, because variables do not represent constant expressions.
  • The order of template parameters matters. The order in the template declaration must match the order in the template argument list unless arguments are explicitly specified.
  • Function pointers as non-type template parameters allow static dispatch:
  • Templates using multiple type parameters allow interfaces to safely operate across heterogeneous data domains without casting or runtime polymorphism.
  • Each unique combination of template arguments results in a separate, specialised instantiation, preserving type safety and performance.
  • Zero Overhead Principle: templates generate only the code required for the types actually used, with no hidden runtime cost.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.