Skip to content

Function_templates

Function templates enable writing generic, reusable code that works with any data type. This chapter covers everything about function templates.

Templates allow you to write functions that work with different types without code duplication. The compiler generates specific versions (instantiations) for each type used.

// Without templates - must duplicate
int max(int a, int b) { return a > b ? a : b; }
double max(double a, double b) { return a > b ? a : b; }
// With templates - single definition
template<typename T>
T max(T a, T b) { return a > b ? a : b; }
#include <iostream>
#include <string>
template<typename T>
T maximum(T a, T b) {
return (a > b) ? a : b;
}
int main() {
// Compiler generates: maximum(int, int)
std::cout << maximum(5, 3) << "\n"; // 5
// Compiler generates: maximum(double, double)
std::cout << maximum(3.14, 2.71) << "\n"; // 3.14
// Compiler generates: maximum(std::string, std::string)
std::cout << maximum(std::string("apple"),
std::string("banana")) << "\n"; // banana
return 0;
}
#include <iostream>
template<typename T, typename U>
std::pair<T, U> makePair(T first, U second) {
return {first, second};
}
int main() {
auto p1 = makePair(1, 3.14); // std::pair<int, double>
auto p2 = makePair("hello", 42); // std::pair<const char*, int>
std::cout << p1.first << ", " << p1.second << "\n";
std::cout << p2.first << ", " << p2.second << "\n";
return 0;
}
// Using typename
template<typename T>
T add(T a, T b) { return a + b; }
// Using class (same as typename)
template<class T>
T multiply(T a, T b) { return a * b; }
// Both are equivalent - typename is preferred
#include <iostream>
// Integer non-type parameter
template<int N>
constexpr int squared() {
return N * N;
}
// Array size as non-type parameter
template<size_t N>
void printArray(const int (&arr)[N]) {
for (size_t i = 0; i < N; i++) {
std::cout << arr[i] << " ";
}
std::cout << "\n";
}
// Pointer as non-type parameter
template<const char* P>
void printString() {
std::cout << P << "\n";
}
int main() {
std::cout << squared<5>() << "\n"; // 25
std::cout << squared<10>() << "\n"; // 100
int arr[] = {1, 2, 3, 4, 5};
printArray(arr);
return 0;
}
#include <vector>
#include <list>
#include <deque>
// Template template parameter
template<typename T, template<typename> class Container>
class Wrapper {
Container<T> data_;
public:
void add(const T& value) { data_.push_back(value); }
size_t size() const { return data_.size(); }
};
int main() {
Wrapper<int, std::vector> vw;
vw.add(1);
vw.add(2);
Wrapper<int, std::list> lw;
lw.add(1);
lw.add(2);
return 0;
}
#include <iostream>
#include <memory>
// Default type parameter
template<typename T = int>
T defaultValue() {
return T{};
}
// Multiple defaults
template<typename T = int, typename U = double>
std::pair<T, U> makePair(U second = U{}) {
return {T{}, second};
}
// Default template template parameter
template<typename T, template<typename> class Container = std::vector>
class ContainerWrapper {
Container<T> data_;
public:
void add(const T& value) { data_.push_back(value); }
};
int main() {
std::cout << defaultValue() << "\n"; // 0
std::cout << defaultValue<double>() << "\n"; // 0
auto p = makePair(3.14);
std::cout << p.first << ", " << p.second << "\n"; // 0, 3.14
return 0;
}
#include <iostream>
#include <string>
template<typename T>
void print(T value) {
std::cout << value << "\n";
}
template<typename T, typename U>
void printPair(T first, U second) {
std::cout << first << ", " << second << "\n";
}
int main() {
print(42); // T = int
print(3.14); // T = double
print("hello"); // T = const char*
printPair(1, 2.5); // T = int, U = double
return 0;
}
#include <iostream>
template<typename T>
T convert(const char* str) {
// Simple conversion (use std::stoi/stod in real code)
return static_cast<T>(str[0] - '0'); // Simplified
}
int main() {
// Explicit template argument
int x = convert<int>("42");
double y = convert<double>("3.14");
std::cout << x << "\n" << y << "\n";
return 0;
}
#include <iostream>
#include <vector>
#include <memory>
// Deduction guide for pair
template<typename T>
pair(T, T) -> pair<T, T>;
// Deduction guide for custom class
template<typename T>
MyContainer(std::initializer_list<T>) -> MyContainer<T>;
int main() {
// Automatically deduces std::pair<int, int>
auto p = std::make_pair(1, 2);
return 0;
}
#include <iostream>
// Primary template
template<typename T>
void process(T value) {
std::cout << "Primary: " << value << "\n";
}
// Overload for pointers
template<typename T>
void process(T* value) {
std::cout << "Pointer: " << *value << "\n";
}
// Overload for const pointers
template<typename T>
void process(const T* value) {
std::cout << "Const pointer: " << *value << "\n";
}
// Specialization for int
template<>
void process<int>(int value) {
std::cout << "Specialized int: " << value << "\n";
}
int main() {
int x = 42;
process(42); // Specialized int
process(&x); // Pointer
process("hello"); // Primary (const char*)
return 0;
}
#include <iostream>
// constexpr function - evaluated at compile time
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
// Can be used in static_assert
static_assert(factorial(5) == 120);
static_assert(factorial(0) == 1);
int main() {
// Compile-time
constexpr int f5 = factorial(5);
std::cout << f5 << "\n"; // 120
// Runtime
int n = 5;
std::cout << factorial(n) << "\n"; // Also 120
return 0;
}
#include <iostream>
#include <type_traits>
template<typename T>
void processValue(T value) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Integer: " << value * 2 << "\n";
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "Float: " << value * 2.0 << "\n";
} else {
// This branch is discarded for integral and floating types
std::cout << "Other type\n";
}
}
int main() {
processValue(42); // Integer: 84
processValue(3.14); // Float: 6.28
processValue("hello"); // Other type
return 0;
}

SFINAE - Substitution Failure Is Not An Error

Section titled “SFINAE - Substitution Failure Is Not An Error”
#include <iostream>
#include <type_traits>
// Only enable for integral types
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
double twice(T value) {
return value * 2.0;
}
// Only enable for floating point
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, T>
double twice(T value) {
return value * 2.0;
}
int main() {
std::cout << twice(21) << "\n"; // 42
std::cout << twice(3.14) << "\n"; // 6.28
// Won't compile: twice("hello")
return 0;
}
#include <iostream>
#include <concepts>
// Using concepts
template<std::integral T>
T doubleValue(T value) {
return value * 2;
}
template<std::floating_point T>
T doubleValue(T value) {
return value * 2.0;
}
int main() {
std::cout << doubleValue(21) << "\n";
std::cout << doubleValue(3.14) << "\n";
return 0;
}
#include <iostream>
template<typename T>
class Wrapper {
T value_;
public:
Wrapper(T v) : value_(v) {}
// Friend function template
template<typename U>
friend Wrapper<std::common_type_t<T, U>>
operator+(const Wrapper<T>& a, const Wrapper<U>& b) {
return Wrapper<std::common_type_t<T, U>>(
a.value_ + b.value_
);
}
};
int main() {
Wrapper<int> a(5);
Wrapper<double> b(3.14);
auto c = a + b; // Wrapper<double>
std::cout << "hello\n";
return 0;
}
#include <iostream>
#include <string>
template<typename T>
void swap(T& a, T& b) {
T temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
int main() {
int x = 1, y = 2;
swap(x, y);
std::cout << x << " " << y << "\n"; // 2 1
std::string s1 = "hello", s2 = "world";
swap(s1, s2);
std::cout << s1 << " " << s2 << "\n"; // world hello
return 0;
}
#include <iostream>
#include <algorithm>
template<typename T>
const T& minimum(const T& a, const T& b) {
return (a < b) ? a : b;
}
template<typename T>
const T& maximum(const T& a, const T& b) {
return (a > b) ? a : b;
}
template<typename T>
std::pair<const T&, const T&> minMax(const T& a, const T& b) {
return (a < b) ? std::pair(a, b) : std::pair(b, a);
}
int main() {
std::cout << minimum(3, 7) << "\n"; // 3
std::cout << maximum(3, 7) << "\n"; // 7
auto [min, max] = minMax(10, 5);
std::cout << min << " " << max << "\n"; // 5 10
return 0;
}
  • Function templates enable generic programming
  • Use typename for type parameters
  • Support multiple template parameters
  • Non-type parameters for compile-time values
  • Automatic argument deduction in most cases
  • Use constexpr for compile-time computation
  • Concepts (C++20) provide cleaner constraints
  • Friend function templates enable operator overloading across types