Template functions, template classes, specialization, variadic templates, static_assert, generic programming, and std::tuple.
Templates allow you to define a behavior (function or class) that works with objects of varying types — without writing separate code for each type. Unlike macros, templates are type-safe: the compiler generates a concrete version for each type you use.
// General syntax
template <parameter list>
// function or class declaration
Template parameters can have defaults:
// T2 defaults to T1's type, T3 defaults to int
template <typename T1, typename T2 = T1, typename T3 = int>
bool TemplateFunction(T1 p1, T2 p2, T3 p3);
A template function works for any type that supports the operations used inside it:
template <typename T>
T GetMax(const T& var1, const T& var2) {
return (var1 > var2) ? var1 : var2;
}
The function can be called explicitly (you specify T) or implicitly (the compiler deduces T from the arguments):
int maxVal = GetMax<int>(num1, num2); // explicit
int maxVal = GetMax(num1, num2); // implicit — compiler deduces T = int
Template classes parameterize the entire class. You can have more than one template parameter:
template <typename T>
class Container {
T value;
public:
Container(T input) : value(input) {}
};
The type must be stated when instantiating a template class:
Container<int> intContainer;
Container<bool> boolContainer;
Member function definitions outside the class also require the template prefix:
template <typename T>
void Container<T>::SomeFunc() {
// ...
}
Sometimes a template needs different behavior for a specific type. You can provide a specialization using template<> (empty parameter list):
// General template
template <typename T>
class Container {
T value;
// ...
};
// Specialization for int — completely different implementation
template<>
class Container<int> {
int value;
std::string extraField; // added for int-specific behavior
// ...
};
Container<int> uses the specialization, everything else uses the general template.Static members in a template class are shared among all instances of the same type parameter. Different template instantiations each get their own separate static:
template <typename T>
class Counter {
public:
static int count; // shared among all Counter<T> of the SAME T
};
// Must be defined outside the class, with the template prefix
template <typename T>
int Counter<T>::count = 0;
// Counter<int>::count and Counter<double>::count are separate variables
Variadic templates accept a variable number of template arguments. The ... syntax represents a parameter pack:
// Base case — handles the last argument
template <typename T>
void printer(T p) {
std::cout << p << std::endl;
}
// Recursive case — peels one argument off Rest each call
template <typename T1, typename... Rest>
void printer(T1 first, Rest... rest) {
printer(first); // print this argument
printer(rest...); // recurse with the remaining arguments
}
printer(1, 2, 3, 4, 5); // works for any number of args
You can check how many arguments remain with sizeof...:
std::cout << sizeof...(Rest) << std::endl; // number of args in Rest
static_assertstatic_assert is a compile-time assert — it halts compilation with a custom error message if a condition is false. It's especially useful for catching invalid template instantiations early:
// Prevent instantiation with int
static_assert(sizeof(T) != sizeof(int), "No ints please!");
// Ensure a type is not a pointer
static_assert(!std::is_pointer<T>::value, "T must not be a pointer type");
assert (runtime, debug-only), static_assert runs at compile time and is always active — there is no Release vs Debug distinction.typename vs classIn template parameter lists, typename and class are identical:
template <class T> // same as...
template <typename T> // ...this
However, there are situations where you must use typename — when referring to a nested dependent type name. The compiler cannot tell if T::SomeType is a type or a static variable, so it assumes it's a variable. You must explicitly tell it otherwise:
// WITHOUT typename — compiler assumes T::const_iterator is a variable
template <typename T>
void Func(T container) {
T::const_iterator x = container.cbegin(); // ERROR
}
// WITH typename — compiler knows it's a type
template <typename T>
void Func(T container) {
typename T::const_iterator x = container.cbegin(); // correct
}
typename. The rule: if a name depends on a template parameter AND refers to a type, it needs typename.Templates support interfaces and polymorphism — but the interfaces are implicit. A template function like this:
template <typename T>
void func(T val) {
if (val.size() > 10 && val != 10) { // ... }
}
…requires that T supports .size() and operator!=. But this is only enforced when the template is instantiated — not when it's defined. If you pass a type that doesn't support those operations, you get a potentially cryptic compile error deep inside the template.
template <typename T> requires std::ranges::sized_range<T>.If you declare a templated copy constructor or assignment operator (e.g. to accept a Container<U> from a Container<T>), you must also declare the normal non-templated versions. Otherwise the compiler generates its own default ones, which may shadow your template:
template <typename T>
class Container {
public:
// Generalized copy — converts Container<U> to Container<T>
template <typename U>
Container(const Container<U>& other);
// Still need the normal copy constructor explicitly
Container(const Container<T>& other);
};
std::tuplestd::tuple is a fixed-size collection of heterogeneous values. Requires #include <tuple>.
#include <tuple>
// Create a tuple with make_tuple
std::tuple<int, char, std::string> myTup = std::make_tuple(5, 'r', "hello");
| Operation | Code | Result |
|---|---|---|
| Size | std::tuple_size<decltype(myTup)>::value | 3 |
| Access by index | std::get<1>(myTup) | 'r' |
| Concatenate | std::tuple_cat(tup1, tup2) | Combined tuple |
| Unpack | std::tie(ignore, cVar, ignore) = myTup; | Unpacks into variables |
std::tie unpacks a tuple into separate variables. Use std::ignore to skip elements you don't need:
char cVar;
std::tie(std::ignore, cVar, std::ignore) = myTup;
std::cout << cVar; // 'r'
std::tie:auto [num, ch, str] = myTup;