C++ Templates for Beginners: Function and Class Templates, Instantiation and Why They Live in Headers

Key takeaways

A template is a recipe the compiler uses to generate a function or class for each set of types you use it with. This post explains how deduction and instantiation work, why template definitions go in headers (and the explicit-instantiation exception), when typename is required, and the real GCC errors behind the most common beginner mistakes.

What a template is

A template is not a function or a class. It is a pattern from which the compiler generates functions or classes when you use it with specific types. Writing

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

defines no code at all by itself. When you call maxOf(3, 5), the compiler works out that T must be int, then generates int maxOf<int>(int, int) from the pattern and compiles it like any other function. Call maxOf(3.5, 2.1) and a second, separate function double maxOf<double>(double, double) is generated. This process is called instantiation.

That is the whole point of templates compared to the alternatives. Without them you either copy the same function for every type (and fix each bug in several places), or you erase the type — void* in C, a common base class in Java-style designs — and give up type checking or pay for a virtual call. A template keeps full static type checking and produces code as efficient as the hand-written version for each type.

Most of what surprises beginners follows from the fact that templates are patterns filled in at the point of use:

  • The compiler needs the full definition wherever the template is used, which is why templates live in headers.
  • A template body is only fully checked when it is instantiated, so a mistake may not show up until someone uses it with a particular type.
  • Each set of template arguments is a separate function or class with its own copy of the code.

Function templates

Argument deduction

#include <iostream>
#include <string>

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

int main() {
    std::cout << maxOf(3, 5) << '\n';                    // T = int
    std::cout << maxOf(3.5, 2.1) << '\n';                // T = double
    std::cout << maxOf(std::string("a"), std::string("b")) << '\n';  // T = std::string
    std::cout << maxOf<double>(3, 5.5) << '\n';          // T given explicitly
}

The compiler deduces T from each argument independently, and the deductions must agree. Mixing types is the first error almost everyone meets:

maxOf(3, 5.0);
error: no matching function for call to 'maxOf(int, double)'
note: candidate: 'template<class T> T maxOf(T, T)'
note:   template argument deduction/substitution failed:
note:   deduced conflicting types for parameter 'T' ('int' and 'double')

Unlike a regular function taking double, deduction never applies implicit conversions: 3 is an int, so the first argument says T = int and the second says T = double. The fixes are to pass the type explicitly (maxOf<double>(3, 5.0)), convert one argument yourself, or use two template parameters if mixing types is genuinely intended.

Name clashes with the standard library

The textbook example is a template named max, and combined with using namespace std; it breaks immediately:

#include <iostream>
using namespace std;

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

int main() { cout << max(3, 5) << '\n'; }
error: call of overloaded 'max(int, int)' is ambiguous
note: candidate: 'T max(T, T) [with T = int]'
note: candidate: 'constexpr const _Tp& std::max(const _Tp&, const _Tp&) [with _Tp = int]'

The file only includes <iostream>, yet std::max is visible — with GCC 10’s standard library, <iostream> happens to pull in the header that declares it. Which standard headers include which others is not specified, so code like this can compile on one compiler and break on the next. Avoiding using namespace std; in anything beyond tiny examples, and not naming your own templates after standard algorithms, sidesteps the whole issue. This post uses maxOf for that reason.

Templates and ordinary overloads together

A non-template function and a template can share a name. When both match equally well, overload resolution prefers the non-template:

template <typename T>
T maxOf(T a, T b) { std::cout << "template "; return (a > b) ? a : b; }

int maxOf(int a, int b) { std::cout << "plain "; return (a > b) ? a : b; }

maxOf(3, 5);      // plain 5     (exact match, non-template wins the tie)
maxOf(3.5, 2.1);  // template 3.5 (only the template matches double exactly)
maxOf<>(3, 5);    // template 5  (empty <> forces the template)

This is the recommended way to give one type special behavior for a function template: add an overload. Explicit specialization of function templates exists, but specializations do not take part in overload resolution themselves, which produces confusing results when overloads are also present. The specialization post linked below goes into that in detail.

Class templates

#include <iostream>
#include <string>

template <typename T>
class Box {
    T value_;
public:
    explicit Box(T v) : value_(std::move(v)) {}
    const T& get() const { return value_; }
    void set(T v) { value_ = std::move(v); }
    void print() const { std::cout << value_ << '\n'; }
};

int main() {
    Box<int> intBox(10);
    Box<std::string> strBox("Hello");
    intBox.print();   // 10
    strBox.print();   // Hello
}

Box<int> and Box<std::string> are two unrelated classes generated from one pattern. They do not share static members, you cannot assign one to the other, and a Box<int>* cannot point at a Box<long>.

A very useful property: member functions of a class template are only instantiated if they are actually used. Box<NoPrint>, where NoPrint has no operator<<, compiles and works as long as nobody calls print(). This is how std::vector<T> can work with types that are not copyable — the members that copy are simply never instantiated for them. The flip side is that a broken member function can sit unnoticed in a header until the first caller uses it with a type that exposes the problem.

Since C++17, class template argument deduction (CTAD) lets you write Box b(10); and have T = int deduced from the constructor; the CTAD post covers the rules and deduction guides.

Multiple parameters and defaults

#include <deque>
#include <vector>

template <typename T, typename Container = std::vector<T>>
class Stack {
    Container data_;
public:
    void push(const T& value) { data_.push_back(value); }
    void pop() { data_.pop_back(); }            // precondition: !empty()
    T& top() { return data_.back(); }            // precondition: !empty()
    bool empty() const { return data_.empty(); }
    std::size_t size() const { return data_.size(); }
};

Stack<int> s1;                     // backed by std::vector<int>
Stack<int, std::deque<int>> s2;    // backed by std::deque<int>

This is exactly how std::stack is designed. Note the preconditions: calling top() or pop() on an empty std::vector is undefined behavior, not an exception. Silently doing nothing in pop() when empty, as many tutorial stacks do, hides bugs in the caller; the standard library chose to make it the caller’s responsibility, and a debug-mode assertion is a better guard than a silent no-op.

The Container parameter also shows a template requirement that is only checked on use: any type with push_back, pop_back, back and empty works. Pass std::list<int> and it works; pass std::set<int> and you get an error pointing inside push, because std::set has no push_back.

Why templates live in headers

Suppose you split a template like an ordinary function:

// maxof.h
template <typename T> T maxOf(T a, T b);

// maxof.cpp
#include "maxof.h"
template <typename T> T maxOf(T a, T b) { return (a > b) ? a : b; }

// main.cpp
#include "maxof.h"
int main() { return maxOf(3, 5); }

Each .cpp file is compiled separately. While compiling main.cpp, the compiler sees the declaration, assumes maxOf<int> exists somewhere and emits a call to it. While compiling maxof.cpp, it sees the definition, but nothing in that file uses maxOf<int>, so it generates nothing. The linker then finds a call with no definition:

undefined reference to `int maxOf<int>(int, int)'
collect2.exe: error: ld returned 1 exit status

This is the most common template error in multi-file projects, and it is confusing precisely because it happens at link time, after every file compiled cleanly. The usual fix is to put the whole definition in the header, so every file that uses the template can instantiate it.

The other fix — often described as impossible, but it is not — is explicit instantiation. If you know the complete set of types in advance, add this to maxof.cpp:

template int maxOf<int>(int, int);   // generate this instantiation here

Now maxof.cpp emits maxOf<int>, the link succeeds, and the program prints 5. Using maxOf<double> from main.cpp would still fail to link until you add a line for it. This approach keeps implementation details out of headers and can cut compile times in large projects, at the cost of a closed list of supported types. A related tool, extern template, tells other files not to instantiate a specialization themselves because one file provides it.

When typename is required

template <typename T> and template <class T> mean exactly the same thing; class here does not mean T must be a class type. Many codebases prefer typename because it reads more accurately.

There is a second use of typename that is not optional. Inside a template, a name that depends on T — such as T::value_type — could be a type or a static member, and the compiler cannot know which until it sees the actual T. By rule, it assumes it is not a type unless you say so:

template <typename T>
void func(const T& c) {
    T::value_type v{};   // error
}
error: need 'typename' before 'T::value_type' because 'T' is a dependent scope

Writing typename T::value_type v{}; fixes it. GCC’s message is unusually clear here, and it is worth remembering, because the follow-up errors (expected ';' before 'v') look like syntax mistakes rather than a missing keyword. C++20 relaxed the rule in some contexts where only a type could appear, but writing typename remains necessary in declarations like this one.

Specializing a class template

When one type needs a completely different implementation, a class template can be specialized:

template <typename T>
class Printer {
public:
    void print(const T& value) const { std::cout << value << '\n'; }
};

template <>
class Printer<bool> {
public:
    void print(bool value) const { std::cout << (value ? "true" : "false") << '\n'; }
};

Printer<int>{}.print(10);     // 10
Printer<bool>{}.print(true);  // true

A full specialization shares nothing with the primary template: every member has to be written again. That makes specialization a good fit for replacing an implementation outright (std::vector<bool> is the famous, somewhat controversial example) and a poor fit for tweaking one member. For a small difference, if constexpr inside the primary template is usually simpler.

Making template errors readable

When a template is used with a type that does not support what the body needs, the error is reported inside the template:

template <typename T>
T add(T a, T b) { return a + b; }

struct Money { int cents; };
add(Money{1}, Money{2});
error: no match for 'operator+' (operand types are 'Money' and 'Money')

Here that is still readable, but in real code the failing line is often several layers deep inside library templates, and the message is followed by a long “required from” chain. The reader has to reconstruct what the template actually needed from where it broke.

C++20 concepts let you state the requirement on the declaration, so the check happens at the call:

#include <concepts>

template <typename T>
    requires std::integral<T> || std::floating_point<T>
T add(T a, T b) { return a + b; }
error: use of function 'T add(T, T) [with T = Money]' with unsatisfied constraints
note: constraints not satisfied
note: no operand of the disjunction is satisfied

The message is not necessarily shorter in trivial cases, but it answers the right question: the call is wrong because Money does not satisfy the stated constraint, rather than because some line inside the implementation failed. Before C++20, a static_assert at the top of the template gives a similar effect with a custom message:

template <typename T>
T add(T a, T b) {
    static_assert(std::is_arithmetic_v<T>, "add() requires a numeric type");
    return a + b;
}

The error trail that confused me most when I started with templates was not a long one but the link-time one above: every file compiled, and the linker then complained about a function I could clearly see defined. Knowing that a template in a .cpp file generates nothing unless something in that same file uses it turns that from a mystery into a two-second fix.

The real costs: compile time and code size

Templates have no dispatch overhead — a maxOf<int> call is as cheap as a hand-written int function, and it is often inlined. The costs show up elsewhere:

  • Compile time. Every file that includes a template-heavy header re-parses it, and every file that uses Foo<int> instantiates it again (the linker then discards the duplicates). Large header-only libraries are a common reason for slow builds; explicit instantiation and extern template are the standard mitigations.
  • Code size. Stack<int>, Stack<long> and Stack<std::string> are three separate copies of the code. Usually that is exactly what you want, but a template instantiated with dozens of types can noticeably grow a binary. A known technique is to move type-independent logic into a non-template base or helper so it exists once.
  • Error discovery. Because bodies are only fully checked on instantiation, a template needs tests that actually instantiate it with representative types. A template header that “compiles fine” may never have been compiled with the types your users will pass.

A mistake I have seen repeatedly is a template that works for every type its author tried and breaks the first time someone passes a const type, a move-only type or a reference. Taking parameters as const T& instead of T where copies are not needed, and testing with at least one move-only type such as std::unique_ptr<int>, catches most of these before users do.

Where to go next

Once function and class templates are comfortable, the natural next steps are variadic templates (templates that accept any number of arguments), specialization and its traps, and concepts for expressing requirements.