A function template is one piece of source code that the compiler stamps out into many real functions, one per type used. The previous lesson covered why templates exist and showed a single-type example. This lesson goes deeper: multiple type parameters, how the compiler figures out which types to use, when to override its choice with explicit arguments, and how templates interact with regular overloads.
The shape is always the same: a template clause that introduces type parameters, then a function definition that uses those parameters in place of concrete types.
typename and class mean the same thing in this position. typename T is the modern convention because it reads clearly when T ends up being something that isn't a class (int, double, a function pointer).
Two type parameters work the same way. Consider a cart line where the quantity is an int and the unit price is a double. The line total has a different type from either input, and the template can express that.
The return type is auto, which tells the compiler "figure out the type of quantity * price and use that". For int * double that's double. For int * int it's int. More on auto returns below.
maxPrice<int>(199, 249) is rarely written. The usual form is maxPrice(199, 249), and the compiler figures out that T is int. That process is called template argument deduction.
The rules sound simple at first: the compiler looks at each argument passed, matches its type against the parameter pattern, and reads T off the match. The wrinkle is that the way the parameter is written changes what gets deduced.
Three parameter shapes come up repeatedly. Each one deduces T differently for the same call.
For a call like byValue(prices) where prices is a std::vector<double>, T deduces to std::vector<double> and the function takes a copy. For byConstRef(prices), T is still std::vector<double> but no copy happens. For byForwardRef(prices), the picture changes: because T&& on a template parameter is a forwarding reference, T becomes std::vector<double>& and the function binds to the original.
What matters here is the practical shape:
| Parameter shape | What T deduces to | What the function receives |
|---|---|---|
T (by value) | The naked type, no const, no reference | A copy |
const T& | The naked type, no const, no reference | A read-only handle |
T&& (forwarding reference) | Could be X or X& depending on the argument | A binding that preserves lvalue/rvalue-ness |
For most templates that only read their arguments, const T& is the right default. Use plain T for tiny types (int, double, a pointer) where copying is as cheap as referencing.
A template that takes T by value copies every argument at every call site. For a std::vector<Product> that's an O(n) copy. Prefer const T& unless a copy is required.
The compiler matches the argument's type against the parameter pattern, peels off cv-qualifiers and references, and reads off T. The pipeline for a single call:
One common surprise: when two arguments must share a type, mixed types fail to deduce.
The compiler sees T = int from 199 and T = double from 19.99 and refuses to pick one. The fix is either to convert one argument (maxPrice(199.0, 19.99)), specify the type explicitly (maxPrice<double>(199, 19.99)), or split into two type parameters (maxPrice(T a, U b)).
Sometimes the compiler can't deduce T, or its choice needs to be overridden. Spell out the type in angle brackets at the call site.
A common case is when T only appears in the return type, not the parameter list. Deduction can't reach into the return type, so it can't figure out T on its own.
Without parsePrice<int> the call wouldn't compile. The parameter is always const std::string&, so T has nothing to deduce against.
The second case is when deduction would pick the wrong type. Consider a findCheapest template that returns the cheapest item from a container, and the container holds Product and the call needs to use the Product interface.
The explicit <Product> is redundant here since deduction would pick the same type, but it documents intent. When code review picks up the call, the reader sees Product immediately and doesn't have to chase the container's element type.
Mixing is also allowed: some arguments explicit, the rest deduced. The explicit ones must come first, in order.
Out can't be deduced (only used in the return type), so the caller passes it. In is deduced from the argument. This pattern shows up constantly in conversion and casting helpers.
Like default function arguments, default template arguments let callers omit a type and fall back to a sensible default. Function templates accept defaults since C++11.
The first call uses the default std::less<double>, which finds the smallest element. The second call passes std::greater<double> explicitly to flip the comparison. The default makes the common case clean while leaving the door open for customization.
Unlike default function arguments, default template arguments don't have to be the last parameter, though placing them anywhere else gets awkward fast. Keep them at the end unless there's a real reason not to.
A function template can coexist with regular functions of the same name, and with other function templates of the same name. The compiler picks the best match using overload resolution rules. The full rule set is long, but two principles cover most realistic situations.
First: when both a template and a non-template match equally well, the non-template wins.
Second: when multiple templates match, the most specialized one wins.
The first rule in action. A generic template handles any type, and a non-template overload handles std::string specifically.
For 42 and 19.99, deduction matches the template exactly with T = int and T = double. For std::string("Notebook"), both the template (with T = std::string) and the non-template match equally. The non-template wins on the tie.
The second rule, "most specialized wins", applies when two templates overlap. A template that takes T* is more specialized than one that takes T, because the pointer version constrains what T can be.
Both templates could match the second call (the first with T = std::string*, the second with T = std::string). The pointer version is more specialized, so it wins.
Overload resolution itself is a compile-time cost, not a runtime one. Once the compiler picks a function, the call is a direct call with no extra dispatch.
The flow for a call site:
The "ambiguous" outcome is the one that causes problems. Adding overloads carelessly can end up with two candidates that each match the call equally well and neither rule breaks the tie. The compiler refuses to pick and emits an error. The fix is usually to constrain one of the overloads (with explicit types, with SFINAE in older code, or with concepts in C++20) so it stops matching the case the other was intended to handle.
For functions that operate on generic input, the right return type often isn't known until the inputs are seen. Modern C++ provides two tools.
auto as a return type tells the compiler "deduce my return type from the return statement". It works for function templates the same way it works for normal functions.
The compiler looks at a + b, computes its type, and stamps that in as the return type. With multiple return statements, they must all deduce to the same type or the function is ill-formed.
auto strips references and const the same way it does for variable declarations. So auto deduces a value, never a reference. To return a reference (for example, to give the caller a handle into a container without copying), use decltype(auto) instead.
If firstItem had used plain auto, it would have returned a double (a copy), and the assignment firstItem(cart) = 24.99 would have changed a temporary rather than the vector element.
decltype(auto) is a power tool. Use it to forward whatever type and reference category return produces. For everyday code, plain auto works.
A function template by itself doesn't generate machine code. Code only appears when the compiler sees a use of the template with concrete types. That use causes the compiler to instantiate the template, meaning it produces a real function with the type parameters filled in.
The common path is implicit instantiation: a call like maxPrice(199, 249) causes the compiler to instantiate maxPrice<int> automatically. The word "instantiate" never appears in the source. This is what happens most of the time.
The translation unit ends up with two separate functions in the compiled output: one for int and one for double. They share no machine code with each other, even though they came from one template.
Every distinct type a template is instantiated with adds a new function to the binary. A template used with 20 different types compiles into 20 copies of similar machine code. This is called code bloat. For most templates it doesn't matter, but for very large generic functions used with many types it can grow binary size noticeably.
Explicit instantiation forces the compiler to emit a particular instantiation in a specific translation unit, even if nothing in that unit uses it. The syntax has no leading template <> clause and uses the type in angle brackets.
This is mostly useful in two situations: hiding template definitions in a .cpp file (so callers in other translation units link against the pre-compiled instantiation) and reducing redundant work when the same template gets instantiated in dozens of translation units. Most lessons in this course don't use explicit instantiations, but it's good to recognize the syntax.
A close cousin is the explicit instantiation declaration with extern, which tells the compiler "don't instantiate this here, somebody else will". It pairs with the definition above in another file and is mostly a build-time optimization for large codebases.
Templates can be constrained further with techniques like specialization and concepts, but those are tools for shaping which types the template accepts, not for controlling when it's instantiated.
10 quizzes