Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 1 of 8

You have written this function. Then you needed it for another type, so you wrote it again:

#include <iostream>
#include <string>

// Three functions, one idea. Change the logic and you change it three times.
int max_int(int a, int b) {
    return (a > b) ? a : b;
}

double max_double(double a, double b) {
    return (a > b) ? a : b;
}

std::string max_string(const std::string& a, const std::string& b) {
    return (a > b) ? a : b;
}

int main() {
    std::cout << max_int(3, 7) << "\n";
    std::cout << max_double(2.5, 1.5) << "\n";
    std::cout << max_string("apple", "banana") << "\n";
    return 0;
}
7
2.5
banana

The bodies are identical. Only the types differ. Add a fourth type and you copy it a fourth time, and now a bug fix has to be applied in four places — which means one of them will be missed.

The same problem is worse for containers. IntVector from lesson 11.6 works for int and nothing else. A DoubleVector would be the same 90 lines with one word changed.

The first bad answer: macros

C solved this with the preprocessor:

#include <iostream>

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int calls = 0;

int expensive(int value) {
    ++calls;
    return value;
}

int main() {
    // The macro pastes its arguments in as text, so expensive(7) appears
    // twice in the expansion and is therefore called twice.
    int result = MAX(expensive(3), expensive(7));

    std::cout << "result " << result << " after " << calls << " calls\n";
    return 0;
}
result 7 after 3 calls

Three calls for two arguments. The macro expands to:

((expensive(3)) > (expensive(7)) ? (expensive(3)) : (expensive(7)))

The winning argument is evaluated a second time to produce the result. If expensive wrote to a file, incremented a counter, or took a second to run, the macro did it twice.

Macros have three more problems:

  • No type checking. MAX("hello", 5) compiles into something meaningless.
  • No scope. A macro is not in a namespace and ignores {} blocks; it applies to every line after the #define.
  • Unreadable errors. The compiler sees the expansion, not the macro, so it reports errors in code you never wrote.

The second bad answer: overloading

C++ lets you reuse the name:

int maximum(int a, int b);
double maximum(double a, double b);
std::string maximum(std::string a, std::string b);

This fixes the type checking, and the call site is clean. It does not fix the duplication — those are still three separate bodies. It is the original problem with better naming.

Templates

#include <iostream>
#include <string>

// One definition. The compiler writes a version for each type you use it with.
template <typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    std::cout << maximum(3, 7) << "\n";
    std::cout << maximum(2.5, 1.5) << "\n";
    std::cout << maximum(std::string("apple"), std::string("banana")) << "\n";
    std::cout << maximum('a', 'z') << "\n";
    return 0;
}
7
2.5
banana
z

One definition, four types, full type checking, each argument evaluated once.

template <typename T> says: what follows is not a function but a recipe for a function, with T standing in for a type to be filled in later.

What the compiler actually does

This is the part worth being precise about, because it explains everything else in this module.

A template is not compiled when you write it. It is compiled when you use it. Each distinct type you call it with causes the compiler to generate a separate function — this is called instantiation.

You can see the result in the compiled binary. Listing the symbols in the program above:

$ nm -C ./maximum | grep maximum
0000000000002b66 W std::string maximum<std::string>(std::string, std::string)
0000000000002bbb W char maximum<char>(char, char)
000000000000293d W double maximum<double>(double, double)
000000000000291d W int maximum<int>(int, int)

Four real functions, at four different addresses, generated from one definition you wrote. The duplication did not disappear — the compiler does it instead of you, which means it cannot drift out of sync.

(nm lists the symbols in a binary; -C un-mangles the names into readable C++. It is worth knowing about for lesson 17.3 on the One Definition Rule.)

The consequences

That "compiled when used" rule has three consequences you will meet constantly:

Templates live in headers. A normal function can be declared in a header and defined in a .cpp. A template cannot — the compiler needs the full body at every call site to instantiate it. Module 17 returns to this.

Errors appear at instantiation, not definition. A template with a mistake in it may compile silently until someone calls it with a type that exposes the mistake. This is why template errors arrive in a wall of text mentioning types you did not write. Lesson 12.8 is entirely about reading those.

Requirements are implicit. maximum requires that T supports >. Nothing in the signature says so. Call it with a type that has no > and you get an error deep inside the body rather than at the call. C++20 concepts fix exactly this, and they are lesson 12.7.

Where you have already used them

Every one of these is a template:

std::vector<int> numbers;          // class template, T = int
std::string text;                  // basic_string<char, ...>
std::unique_ptr<Widget> owner;     // class template
std::sort(v.begin(), v.end());     // function template
std::make_unique<Widget>();        // function template

You have been using templates since Module 4. This module is about writing them.

Try it

Write swap_values(a, b) three times — once as a macro, once as three overloads, and once as a template. Call each with int, double, and std::string. Then find an argument that makes the macro version behave wrongly while the template version is fine.

Checkpoint

  • Templates exist to remove the copy-per-type duplication, not as an advanced feature.
  • Macros do it without type checking, without scope, and by evaluating arguments more than once.
  • Overloading fixes type checking but keeps the duplicate bodies.
  • template <typename T> defines a recipe; the compiler generates a real function per type.
  • That generation is called instantiation, and you can see the results with nm -C.
  • Templates must live in headers, because the body is needed at every call site.
  • Errors appear where a template is used, not where it is written.
  • Requirements on T are implicit until C++20 concepts make them explicit.

Questions about this lesson

Log in to ask a question.