Function Templates in C++: Syntax, Invocation, and Overload Resolution
Function templates enable writing generic algorithms that operate on different data types without sacrificing type safety. A template declaration begins with the template keyword followed by a list of template parameters enclosed in angle brackets.
template<typename T>
void Exchange(T& first, T& second) {
T intermediate = first;
first = second;
second = intermediate;
}
void DemonstrateExchange() {
int x = 5, y = 9;
double a = 2.3, b = 7.1;
Exchange(x, y); // automatic type deduction
Exchange<int>(x, y); // explicit type argument
Exchange(a, b);
}
When relying on automatic deduction, all function arguments that correspond to the same template parameter must deduce the same type. If the compiler cannot deduce a consistent T, the code fails to compile. Additionally, a function template cannot be called unless every template parameter is either deduced or explicitly provided.
template<typename T>
void Swap(T& left, T& right) {
T temp = left;
left = right;
right = temp;
}
template<typename T>
void Action() {
// ...
}
void DemoConstraints() {
int m = 1, n = 2;
char c = 'A';
Swap(m, n); // OK: T deduced as int
// Swap(m, c); // Error: cannot deduce a common T for int and char
// Action(); // Error: T is not deducible
Action<double>(); // OK: explicit specification
}
A template can operate on arrays. The following example implements a bubble sort algorithm for any type that supports comparison operators, together with a helper template that prints array contents.
template<typename T>
void SwapElements(T& x, T& y) {
T tmp = x;
x = y;
y = tmp;
}
template<typename T>
void BubbleSort(T data[], int size) {
for (int i = 0; i < size - 1; ++i) {
for (int j = 0; j < size - 1 - i; ++j) {
if (data[j] > data[j + 1]) {
SwapElements(data[j], data[j + 1]);
}
}
}
}
template<typename T>
void ShowArray(T arr[], int len) {
for (int idx = 0; idx < len; ++idx) {
std::cout << arr[idx] << ' ';
}
std::cout << std::endl;
}
void TestCharacterArray() {
char letters[] = {'z', 'd', 'a', 'k', 'm', 'b', 'e'};
int length = sizeof(letters) / sizeof(char);
BubbleSort(letters, length);
ShowArray(letters, length);
}
void TestIntegerArray() {
int values[] = {42, 11, 74, 3, 56, 28, 19};
int count = sizeof(values) / sizeof(int);
BubbleSort(values, count);
ShowArray(values, count);
}
Ordinary functions and function templates differ in how implicit conversions are handled:
- A normal function may silently convert arguments to match paramter types.
- A function template using automatic deduction never performs implicit conversions on the deduced template arguments.
- When template arguments are supplied explicitly, the compiler may apply standard implicit conversions to the function parameters.
int AddIntegers(int lhs, int rhs) {
return lhs + rhs;
}
template<typename T>
T GenericAdd(T lhs, T rhs) {
return lhs + rhs;
}
void TestConversions() {
int a = 20;
char ch = 'A'; // ASCII 65
std::cout << AddIntegers(a, ch) << std::endl; // implicit conversion char -> int
// std::cout << GenericAdd(a, ch); // error: deduction conflict
std::cout << GenericAdd<int>(a, ch) << std::endl; // explicit <int> allows conversion
}
When both a regular function and a matching template are visible, overload resolution follows these guidelines:
- If an ordinary function provides an exact match, it is preferred over the template.
- An empty template argument list (
<>) forces the compiler to select the function template. - Function templates can be overloaded with different parameter sets.
- The template wins if it yields a better match than any non-template function.
void Display(int, int) {
std::cout << "Ordinary function called" << std::endl;
}
template<typename T>
void Display(T, T) {
std::cout << "Function template called" << std::endl;
}
void TestOverloadResolution() {
int u = 1, v = 2;
Display(u, v); // ordinary function is preferred
Display<>(u, v); // force template instantiation
char c1 = 'x', c2 = 'y';
Display(c1, c2); // template gives a closer match, so template wins
}
Generic implementations may not be appropriate for every type. For example, assigning one array name to another does not copy elements, and comparing custom objects with == is unsafe unless the operator is defined. Template specialization solves this by providing tailored implementations for specific types.
class Person {
public:
Person(const std::string& name, int age)
: m_name(name), m_age(age) {}
std::string m_name;
int m_age;
};
template<typename T>
bool AreEqual(const T& a, const T& b) {
return a == b;
}
// Explicit specialization for Person
template<>
bool AreEqual(const Person& lhs, const Person& rhs) {
return lhs.m_name == rhs.m_name && lhs.m_age == rhs.m_age;
}
void TestComparison() {
int i1 = 10, i2 = 10;
std::cout << (AreEqual(i1, i2) ? "equal" : "not equal") << std::endl;
Person alice("Alice", 30);
Person anotherAlice("Alice", 30);
std::cout << (AreEqual(alice, anotherAlice) ? "equal" : "not equal") << std::endl;
}