Skip to content
C Codeloom
C++

C++ Templates: Generics That Compile to Specifics

Learn C++ templates from scratch: function templates, class templates, type deduction, specialization, and how the compiler stamps out concrete code.

·5 min read · By Yash Kesharwani
Intermediate 9 min read

What you'll learn

  • Write function and class templates
  • Understand template argument deduction
  • Use non-type template parameters and defaults
  • Apply specialization when one type needs different behavior
  • Know why template code lives in headers

Prerequisites

  • C++ classes — see /blog/cpp-classes-and-objects

Templates are how C++ does generics. Unlike Java or C# generics, templates are not runtime polymorphism — the compiler generates a fresh copy of your code for each type you use. That is why std::vector<int> and std::vector<double> run at exactly the speed of hand-written code.

A function template

The minimal template is a function with a type parameter.

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

int    i = max_of(3, 7);
double d = max_of(2.5, 1.9);

typename and class are interchangeable in this position; typename is the more modern style. The compiler infers T from the arguments.

Argument deduction

You can also spell the type explicitly:

auto x = max_of<long>(2, 5);   // forces T = long

Explicit arguments are useful when deduction is ambiguous or when only the return path depends on T.

Multiple type parameters

template <typename A, typename B>
auto add(A a, B b) {
    return a + b;
}

auto r = add(1, 2.5);   // double, deduced

The return type is deduced from the body. For older standards, write -> decltype(a + b) after the parameter list.

Class templates

template <typename T>
class Box {
public:
    Box(T value) : value_(std::move(value)) {}
    const T& get() const { return value_; }
    void     set(T v)    { value_ = std::move(v); }

private:
    T value_;
};

Box<int> a(42);
Box<std::string> b("hi");

Each distinct T produces a distinct class. Box<int> and Box<std::string> share source but not binary.

Class template argument deduction (CTAD)

C++17 lets you skip the type when the constructor makes it obvious.

Box a(42);              // Box<int>
Box b(std::string{"x"}); // Box<std::string>

CTAD reads cleanly and removes redundancy.

Non-type template parameters

Templates can take values too, not just types. These are useful for compile-time sizes.

template <typename T, std::size_t N>
struct Array {
    T data[N];
    constexpr std::size_t size() const { return N; }
};

Array<int, 8> a;

std::array in the standard library is essentially this with more polish.

Default template arguments

Like default function arguments, you can supply defaults.

template <typename T = int>
T zero() { return T{}; }

auto z = zero();        // int
auto d = zero<double>(); // double

Specialization

Sometimes one type needs different behavior. Full specialization replaces the template body for a specific type.

template <typename T>
struct Printer {
    static void print(const T& v) { std::cout << v; }
};

template <>
struct Printer<bool> {
    static void print(bool v) { std::cout << (v ? "true" : "false"); }
};

For function templates, prefer overloading over specialization — the rules are simpler.

constexpr if

Inside a template, if constexpr selects branches based on type traits at compile time. The unselected branch is not even instantiated.

#include <type_traits>

template <typename T>
void describe(T v) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << "integer: " << v;
    } else if constexpr (std::is_floating_point_v<T>) {
        std::cout << "float: " << v;
    } else {
        std::cout << "other";
    }
}

This replaces the SFINAE tricks that older C++ required.

Concepts (C++20)

Concepts let you constrain template parameters with readable predicates.

#include <concepts>

template <std::integral T>
T half(T x) { return x / 2; }

half(10);     // ok
// half(2.5); // error — double is not integral

The error message points at the constraint failure, not a wall of instantiation noise.

Why template code lives in headers

The compiler must see the template definition at every point of use to stamp out the right instantiation. Putting the body in a .cpp file usually causes linker errors. Either:

  • Put the entire definition in the header, or
  • Explicitly instantiate the types you use in a .cpp file.
// in box.cpp, after the class definition
template class Box<int>;
template class Box<std::string>;

Explicit instantiation is useful when compile times balloon and you only need a small fixed set of types.

A small worked example

A type-safe pair printer:

#include <iostream>
#include <utility>

template <typename A, typename B>
void print_pair(const std::pair<A, B>& p) {
    std::cout << '(' << p.first << ", " << p.second << ')';
}

int main() {
    print_pair(std::pair{1, "one"});
    print_pair(std::pair{3.14, true});
}

One template, two instantiations, zero runtime cost.

Common pitfalls

  • Putting template definitions in a .cpp and getting undefined reference.
  • Forgetting typename when naming a dependent type inside a template.
  • Letting error messages scroll without scanning for the first concrete type involved.
  • Overusing specialization where an overload would do.

What is next

Now that you can write generic code, see how the standard library uses templates everywhere in STL Containers. After that, Smart Pointers shows class templates managing ownership.

Wrap up

Templates produce zero-overhead generic code by generating a specialization per use site. Put definitions in headers, deduce arguments when you can, reach for if constexpr and concepts to keep branching readable. The cost is a steeper compiler and slower builds; the payoff is reusable code with no runtime tax.