You need max for int, double and std::string. Compare writing three overloads with writing one template. Explain template instantiation, and why template code usually lives in header files.
Show the explanation
Overloading: several functions share a name but have different parameter lists. The compiler picks one by overload resolution: exact match first, then promotions, then conversions. You write and maintain every version yourself. That is useful when the versions really differ, for example a faster special case for one type.
Template: you write the logic once, with the type as a parameter:
template <typename T>
const T &maxOf(const T &a, const T &b) { return (a < b) ? b : a; }
maxOf(3, 7); // T = int (deduced)
maxOf(std::string("ab"), std::string("b")); // T = std::stringThe only requirement on T is what the body uses, here operator<. C++20 concepts let you state that requirement explicitly (template <std::totally_ordered T>), which gives much clearer error messages.
Instantiation. A template is not code until it is used. When the compiler sees maxOf(3, 7), it deduces T = int and generates a separate function maxOf<int>. Every distinct T gets its own copy. This is compile-time (static) polymorphism. There is no run-time cost, and calls can be inlined, but many instantiations make the binary larger and compilation slower.
Why templates live in headers. To instantiate maxOf<int>, the compiler needs to see the template's body in the file it is compiling. If the body sat in a .cpp file, other files could only see a declaration, and you would get "undefined reference" linker errors. The workarounds are explicit instantiation (template const int &maxOf<int>(const int&, const int&);) or C++20 modules.
Deduction gotcha: maxOf(3, 7.5) fails, because T cannot be both int and double. Write maxOf<double>(3, 7.5) or cast one argument.
Specialisation: you can provide a custom version for one type with template <> .... For functions, an ordinary overload is usually clearer.
Compared with virtual functions: templates choose the behaviour at compile time from types known then. Virtual functions choose at run time, from the actual object.
Original UniCircle question. Last reviewed 22 Oct 2026.
