Skip to content
Home
C++ Templates: Generic Programming and Metaprogramming

C++ Templates: Generic Programming and Metaprogramming

C/C++ C/C++ 8 min read 1625 words Beginner ExcellentWiki Editorial Team

C++ templates enable generic programming — writing code that works with any type. Templates are compile-time mechanisms: the compiler generates specialized code for each type you use. This gives you zero-cost abstractions: generic code that is as efficient as hand-written, type-specific code. Templates also form the foundation of template metaprogramming, where computations are performed at compile time.

Function Templates

A function template is a blueprint for generating functions.

template <typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
---

// Usage — compiler deduces T
int x = max(3, 7);        // T = int
double y = max(3.14, 2.72); // T = double
std::string s = max(std::string("apple"), std::string("banana"));

You can explicitly specify the template argument:

auto z = max<double>(3, 7.5);  // Forces T = double, 3 is promoted to 3.0

Multiple Template Parameters

template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
    return a + b;
---

// C++14 and later: use auto return type
template <typename T, typename U>
auto add(T a, U b) {
    return a + b;
---

Template Parameter Types

Template parameters can be types, non-type values, or templates:

template <typename T, int N>
class Array {
    T data[N];
public:
    int size() const { return N; }
---;

Array<int, 100> arr;  // 100-element array of int

Class Templates

Class templates define families of classes parameterized by types or values.

template <typename T>
class Stack {
    std::vector<T> elements;
public:
    void push(const T& value) { elements.push_back(value); }
    T pop() {
        T value = elements.back();
        elements.pop_back();
        return value;
    }
    bool empty() const { return elements.empty(); }
---;

Stack<int> intStack;
intStack.push(42);

Stack<std::string> stringStack;
stringStack.push("hello");

Template Member Functions

Member functions of class templates are themselves function templates:

template <typename T>
class Wrapper {
    T value;
public:
    Wrapper(const T& v) : value(v) {}

    template <typename U>
    Wrapper<U> convert() const {
        return Wrapper<U>(static_cast<U>(value));
    }
---;

Template Specialization

Full Specialization

Provide a specific implementation for a particular type:

// Primary template
template <typename T>
struct IsPointer {
    static constexpr bool value = false;
---;

// Full specialization for pointer types
template <>
struct IsPointer<int*> {
    static constexpr bool value = true;
---;

Partial Specialization

Specialize for a subset of types:

// Primary template
template <typename T, typename U>
struct Pair { /* generic */ };

// Partial specialization: both types are the same
template <typename T>
struct Pair<T, T> { /* optimized for identical types */ };

// Partial specialization: second type is void
template <typename T>
struct Pair<T, void> { /* handle void second type */ };

Partial specialization is particularly useful for traits and type transformations:

template <typename T>
struct RemoveReference {
    using type = T;
---;

template <typename T>
struct RemoveReference<T&> {
    using type = T;
---;

template <typename T>
struct RemoveReference<T&&> {
    using type = T;
---;

// Usage
RemoveReference<int&>::type  // int

Variadic Templates

Variadic templates accept any number of template arguments:

// Base case
void print() {}

// Recursive variadic function
template <typename T, typename... Args>
void print(const T& first, const Args&... rest) {
    std::cout << first << ' ';
    print(rest...);
---

print(1, 2.5, "hello", 'c');  // Prints: 1 2.5 hello c

Fold Expressions (C++17)

Simplify variadic templates:

template <typename... Args>
auto sum(Args... args) {
    return (args + ...);  // Unary right fold
---

template <typename... Args>
bool allTrue(Args... args) {
    return (args && ...);  // Unary right fold
---

template <typename... Args>
void printAll(const Args&... args) {
    ((std::cout << args << ' '), ...);  // Fold over comma operator
---

Template Metaprogramming Basics

Template metaprogramming performs computations at compile time using template instantiation.

Compile-Time Factorial

template <unsigned int N>
struct Factorial {
    static constexpr unsigned int value = N * Factorial<N - 1>::value;
---;

template <>
struct Factorial<0> {
    static constexpr unsigned int value = 1;
---;

// Factorial<5>::value == 120 (computed at compile time)

Compile-Time Type Traits

template <typename T>
struct TypeInfo {
    static constexpr bool is_integral = false;
    static constexpr bool is_floating_point = false;
---;

template <>
struct TypeInfo<int> {
    static constexpr bool is_integral = true;
    static constexpr bool is_floating_point = false;
---;

template <>
struct TypeInfo<double> {
    static constexpr bool is_integral = false;
    static constexpr bool is_floating_point = true;
---;

Modern C++ provides these in <type_traits>:

#include <type_traits>

static_assert(std::is_integral_v<int>);
static_assert(std::is_floating_point_v<double>);
static_assert(std::is_const_v<const int>);

SFINAE (Substitution Failure Is Not An Error)

When template argument deduction fails for a particular overload, the compiler doesn’t error — it simply removes that overload from consideration.

template <typename T>
auto increment(T value) -> decltype(value + 1) {
    return value + 1;
---

// Only enabled for types with a .size() member
template <typename T>
auto getSize(const T& container) -> decltype(container.size()) {
    return container.size();
---

// With enable_if
template <typename T>
std::enable_if_t<std::is_integral_v<T>, T>
half(T value) {
    return value / 2;
---

template <typename T>
std::enable_if_t<std::is_floating_point_v<T>, T>
half(T value) {
    return value / 2.0;
---

constexpr and if constexpr (C++17)

constexpr simplifies compile-time programming significantly:

template <typename T>
auto process(const T& value) {
    if constexpr (std::is_integral_v<T>) {
        return value * 2;
    } else if constexpr (std::is_floating_point_v<T>) {
        return value / 2.0;
    } else {
        return value;
    }
---

Unlike regular if, if constexpr discards the unused branch at compile time, so types in the discarded branch don’t need to be valid.

Concepts (C++20)

Concepts constrain template parameters, providing better error messages and clearer interfaces:

#include <concepts>

template <typename T>
concept Integral = std::is_integral_v<T>;

template <typename T>
concept Hashable = requires(T a) {
    { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
---;

template <Integral T>
T doubleValue(T value) {
    return value * 2;
---

template <Hashable T>
void useHash(const T& value) {
    auto h = std::hash<T>{}(value);
---

Common Patterns

CRTP (Curiously Recurring Template Pattern)

template <typename Derived>
class Shape {
public:
    double area() const {
        return static_cast<const Derived*>(this)->area_impl();
    }
---;

class Circle : public Shape<Circle> {
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area_impl() const { return 3.14159 * radius * radius; }
---;

Policy-Based Design

template <typename T, typename ThreadingModel>
class Container {
    ThreadingModel lock;
public:
    void insert(const T& value) {
        lock.lock();
        // insert logic
        lock.unlock();
    }
---;

struct SingleThreaded {
    void lock() {}
    void unlock() {}
---;

struct MultiThreaded {
    void lock() { mutex.lock(); }
    void unlock() { mutex.unlock(); }
    std::mutex mutex;
---;

Conclusion

Templates are the foundation of generic C++ programming. Use function templates for type-generic algorithms, class templates for flexible data structures, and variadic templates for type-safe variadic functions. For compile-time computation, prefer constexpr and if constexpr over classical template metaprogramming in modern C++. When writing constrained templates, use C++20 concepts for clearer interfaces and better diagnostics.

Related: See our C++ STL guide and C++ memory management.

Template Instantiation

Templates are blueprints — they are not compiled until instantiated with concrete types. Each instantiation creates a separate version of the function or class with the given template arguments. This means template code must be visible at the point of use, which is why templates are typically defined in header files rather than source files.

Explicit vs Implicit Instantiation

template<typename T>
T max(T a, T b) { return a > b ? a : b; }

// Implicit instantiation — compiler deduces the type
int m = max(3, 7);     // Instantiates max<int>
double d = max(3.14, 2.71);  // Instantiates max<double>

// Explicit instantiation — force compilation of specific types
template double max(double, double);

Explicit instantiation is useful for hiding template implementations in source files (reducing compile time) and for controlling which type combinations are allowed.

Template Specialization

Full specialization provides a custom implementation for a specific template argument:

template<typename T>
struct IsVoid { static constexpr bool value = false; };

template<>
struct IsVoid<void> { static constexpr bool value = true; };

Partial specialization (available only for class templates) customizes the template for a category of types:

template<typename T>
struct RemoveReference { using type = T; };

template<typename T>
struct RemoveReference<T&> { using type = T; };

template<typename T>
struct RemoveReference<T&&> { using type = T; };

Variadic Templates

Variadic templates accept an arbitrary number of template arguments:

template<typename... Args>
void print_all(Args... args) {
    (std::cout << ... << args) << std::endl;  // C++17 fold expression
---

print_all(1, "hello", 3.14, 'c');  // Prints everything

Fold expressions (... operator) reduce parameter packs with binary operators. C++17 supports unary and binary fold expressions for +, *, &&, ||, ,, and other operators.

Template Template Parameters

Templates can accept other templates as parameters, enabling powerful abstractions:

template<typename T, template<typename> class Container = std::vector>
class MyCollection {
    Container<T> data;
---;

MyCollection<int> col;  // Uses std::vector<int>
MyCollection<int, std::deque> col2;  // Uses std::deque<int>

FAQ

What is the difference between malloc and new in C++? malloc allocates raw memory without calling constructors; new allocates memory and calls the constructor. In C++, prefer new for objects. free vs delete follows the same pattern — delete calls the destructor.

How do I prevent memory leaks in C/C++? Use RAII (Resource Acquisition Is Initialization) in C++ — smart pointers like std::unique_ptr and std::shared_ptr automatically free memory. In C, always pair every malloc with a free and use tools like Valgrind or AddressSanitizer to detect leaks.

What is undefined behavior in C/C++? Undefined behavior occurs when code performs operations that the language standard does not define — dereferencing a null pointer, buffer overflow, signed integer overflow. The compiler may generate any code, including unexpected results or crashes.

Should I learn C or C++ first? Learn C first if you want to understand low-level memory and system programming. Learn C++ first if you want object-oriented features and the STL. Both are valuable; C++ builds on C concepts.

What is the difference between a header file and a source file? Header files (.h) declare interfaces — function prototypes, class definitions, macros. Source files (.c or .cpp) implement the declarations. Headers are #included; source files are compiled separately and linked.

Section: C/C++ 1625 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top