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
Tare implicit until C++20 concepts make them explicit.
Questions about this lesson
Log in to ask a question.