Write code that works for any type: function and class templates, full and partial specialization, variadic templates, and constraining types with type traits, SFINAE, and concepts.
A template is a recipe the compiler stamps out for each type you use it with — generic code with zero runtime cost. This is how std::vector<int> and std::vector<std::string> come from one definition. The type parameter is written <typename T>.
#include <iostream>
template <typename T>
T maxOf(T a, T b) { return a > b ? a : b; } // works for any comparable T
template <typename T>
class Box {
T value_;
public:
explicit Box(T v) : value_(std::move(v)) {}
const T& get() const { return value_; }
};
int main() {
std::cout << maxOf(3, 7) << "\n"; // int -> 7
std::cout << maxOf(2.5, 1.5) << "\n"; // double-> 2.5
Box<std::string> b{"hi"};
std::cout << b.get() << "\n"; // hi
return 0;
}Sometimes one type needs different handling. A full specialization replaces the template entirely for a specific type; a partial specialization customizes a subset of cases (for example, all pointer types). The compiler picks the most specific match.
#include <iostream>
template <typename T>
struct TypeName { static const char* get() { return "unknown"; } };
template <> // full specialization for int
struct TypeName<int> { static const char* get() { return "int"; } };
template <typename T> // partial specialization: any pointer
struct TypeName<T*> { static const char* get() { return "pointer"; } };
int main() {
std::cout << TypeName<int>::get() << "\n"; // int
std::cout << TypeName<double>::get() << "\n"; // unknown
std::cout << TypeName<char*>::get() << "\n"; // pointer
return 0;
}A variadic template accepts any number of arguments of any types via a parameter pack (typename... Args). Fold expressions (C++17) collapse the pack with an operator — here a single (... << args) prints them all. This is how type-safe printf-style functions are written.
#include <iostream>
template <typename... Args>
void print(const Args&... args) {
((std::cout << args << ' '), ...); // fold over the comma operator
std::cout << "\n";
}
template <typename... Args>
auto sum(Args... args) {
return (args + ...); // fold over +
}
int main() {
print("id", 42, 3.14, 'x'); // id 42 3.14 x
std::cout << sum(1, 2, 3, 4) << "\n"; // 10
return 0;
}Type traits (<type_traits>) answer compile-time questions like "is this an integer?". Historically templates were constrained with SFINAE — an unwieldy trick where an invalid substitution quietly removes an overload. C++20 concepts replace it with readable requirements and far clearer error messages.
#include <iostream>
#include <type_traits>
#include <concepts>
// C++20 concept: only accept integral types
template <std::integral T>
T doubleIt(T x) { return x * 2; }
// the older SFINAE way, for comparison:
template <typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
T tripleIt(T x) { return x * 3; }
int main() {
std::cout << doubleIt(21) << "\n"; // 42
std::cout << tripleIt(14) << "\n"; // 42
// doubleIt(3.14); // error: 3.14 is not integral — clear message
return 0;
}