To declare a function template, you use the template keyword followed by a list of template parameters enclosed in angle brackets (<>), and then the function's signature. The template parameters act as placeholders for the types or values that will be specified later when the template is used.
Basic syntax
The fundamental syntax for a function template is as follows:
template <typename T>
return_type function_name(T parameter1, ...);
Use code with caution.
template: A required keyword indicating that a template definition is about to follow.<typename T>: The template parameter list.typename(orclass) is a keyword that specifiesTis a type placeholder. You can use any valid identifier for the placeholder, butTis a common convention.return_type function_name(T parameter1, ...): The standard function declaration, but with the generic typeTused for one or more parameters or the return type.
Example: A simple max function
A classic example is a function that returns the larger of two values. Without templates, you would need to overload the function for each data type you want to support (e.g., int, float, double).
// Non-template function overloads
int max(int a, int b) { return (a > b) ? a : b; }
double max(double a, double b) { return (a > b) ? a : b; }
Use code with caution.
Using a function template, you can achieve the same result with a single, generic function definition:
// Template function
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
Use code with caution.
How it works
When you call the template function, the compiler automatically generates a concrete function for the data types you pass. This process is called template instantiation.
#include <iostream>
int main() {
// The compiler instantiates max(int, int)
std::cout << max(10, 20) << std::endl; // Prints 20
// The compiler instantiates max(double, double)
std::cout << max(10.5, 20.5) << std::endl; // Prints 20.5
// The compiler instantiates max(char, char)
std::cout << max('a', 'z') << std::endl; // Prints 'z'
}
Use code with caution.
Advanced template declaration
Multiple template parameters
You can declare multiple template parameters to handle functions that operate on different types simultaneously.
template <typename T, typename U>
void print_pair(T first, U second) {
std::cout << "First: " << first << ", Second: " << second << std::endl;
}
// Example usage
print_pair(42, "hello"); // T is int, U is const char*
Use code with caution.
Non-type template parameters
Templates can also take constant values as parameters, which must be known at compile time. This is useful for creating generic types with fixed sizes, like std::array.
template <typename T, size_t Size>
void print_array(const T (&arr)[Size]) {
for (size_t i = 0; i < Size; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}
int main() {
int arr[] = {1, 2, 3, 4};
print_array(arr); // Size is deduced as 4
}
Use code with caution.
Default template arguments
Default values can be provided for template parameters, similar to regular function parameters.
template <typename T = int>
T sum_two(T a, T b) {
return a + b;
}
// Example usage
sum_two(5, 10); // Uses default T=int
sum_two<double>(5.5, 10.5); // Explicitly specifies T=double
Use code with caution.
Important considerations
One-Definition Rule (ODR) and template implementation
A frequent point of confusion is where to place template definitions. Unlike regular functions, which are often declared in a header file (.h) and defined in a source file (.cpp), templates must be available to the compiler at the point of instantiation.
This means the full template definition (declaration and implementation) must be placed in a header file. The linker needs access to this definition to generate the function for each concrete type, and separating them across files can lead to linker errors.
my_template.h
#ifndef MY_TEMPLATE_H
#define MY_TEMPLATE_H
template <typename T>
T add(T a, T b) {
return a + b;
}
#endif
Use code with caution.
Explicit instantiation
In some situations, the template definition can be placed in a source file to reduce compile time. This is possible through explicit instantiation, where the compiler is instructed to generate specific versions of the template.
my_template.h
#ifndef MY_TEMPLATE_H
#define MY_TEMPLATE_H
template <typename T>
T add(T a, T b); // Declaration only
#endif
Use code with caution.
my_template.cpp
#include "my_template.h"
template <typename T>
T add(T a, T b) {
return a + b;
}
// Explicitly instantiate the template for int and double
template int add<int>(int, int);
template double add<double>(double, double);
Use code with caution.
If add is called with a type not explicitly instantiated, a linker error will result.
Template specialization
Template specialization allows you to define a specific, non-templated implementation for a particular data type. This is helpful when the generic version doesn't handle a specific type correctly or optimally.
// Generic template
template <typename T>
void print(T value) {
std::cout << "Generic print: " << value << std::endl;
}
// Explicit specialization for const char* (C-style strings)
template <>
void print<const char*>(const char* value) {
std::cout << "Specialized for C-string: " << value << std::endl;
}
int main() {
print(123); // Calls generic template
print("hello"); // Calls specialized version
}
Use code with caution.
Concepts (C++20)
C++20 introduced concepts for more robust and readable code. Concepts let you enforce constraints on template parameters, which improves type safety and provides clearer error messages when a type doesn't meet the requirements.
#include <concepts>
#include <iostream>
// The `std::totally_ordered` concept requires that a type has comparison operators
template <std::totally_ordered T>
T max_ordered(T a, T b) {
return (a > b) ? a : b;
}
struct Unordered {};
int main() {
std::cout << max_ordered(10, 20) << std::endl; // OK
// max_ordered(Unordered{}, Unordered{}); // Fails to compile, with clear error
}
Use code with caution.