C++ Part I · Module 18 · Templates and Generic Programming
Function Templates
The syntax is three words and a parameter list, and it is the smaller half of this chapter. What matters more is a claim people make about templates that is not quite true: that a function template works for any type. It does not. It works for any type that supports the operations its body happens to use, and for a function returning the smaller of two values that is exactly one operation — the less-than comparison. Everything else people assume is required is not. This chapter measures that by building a type with no default constructor, no copy constructor, no assignment, and nothing at all except a friend operator less-than, and passing it to the template, where it works. The reason this is worth measuring rather than asserting is that the requirement is invisible. Nothing in template angle-bracket typename T const T ampersand smaller says that T must be comparable; the requirement lives in the body, is discovered by the compiler at the moment somebody supplies a type, and before C++20 there is no construct that states it in the declaration where a reader would look. So the honest practice is to write the requirement down as a comment and keep it accurate — which this chapter does, and which the frozen description of it puts on equal footing with the syntax.
Intermediate17 min readC++Part IFundamentalsTemplates
Part I · Module 18 · Chapter 18.2 · Function Templates
1. The Syntax
template <typename T>
const T& smaller(const T& a, const T& b) { return b < a ? b : a; }Two lines, and every part earns its place.
template <typename T> introduces a template parameter list. T is a name for a type that will be supplied later; the declaration that follows is written in terms of it.
typename says T is a type. class means exactly the same thing here and is older; typename reads better because T need not be a class — Chapter 18.1 §9's point that templates work on int.
const T& is Chapter 6.5's rule applied generically: take by const reference, because T might be a std::string and copying would be wasteful.
And returning const T& returns one of the arguments rather than a copy — which is correct here and has a hazard §7 covers.
2. Using It
smaller(3, 7); // T is int
smaller(2.5, 1.5); // T is doubleNo angle brackets at the call site. The compiler works T out from the argument types — Chapter 18.3 is the chapter on how, and on the several cases where it cannot.
Chapter 18.1 §5 measured what this produces: for T = int, five instructions byte-identical to a hand-written int version. The template is not a wrapper around anything; it became that function.
3. What It Actually Requires
Here is the claim worth testing. Read the body and list what T must support:
return b < a ? b : a;One operation: b < a must be valid, and its result must be usable as a condition. That is the whole requirement.
Not comparable in general, not copyable, not default-constructible, not assignable — none of those appear in the body, so none of them is required. Measured, with a type that has only a less-than:
struct JustLess {
int v;
friend bool operator<(const JustLess& a, const JustLess& b) { return a.v < b.v; }
};A. a type with ONLY operator< works:
smaller({7},{3}).v = 3JustLess has no default constructor, no copy constructor, no assignment operator — and the call compiled and returned the right answer, because the template never needed any of them.
And the requirement is looser still than "returns bool":
C. a type whose operator< returns int (not bool) also works:
smaller({7},{3}).v = 3 (int was contextually converted to bool)An operator< returning int is fine, because ?: needs a condition and an int converts contextually. The requirement is usable as a condition, not is a bool.
Finally, the obvious case:
D. std::string works, because it has operator<:
smaller("pear","apple") = appleA template's requirements are the operations its body performs — no more and no less. They are not a property of the type; they are a property of the body.
4. The Requirement Is Invisible
Now the problem, and it is the reason the frozen description of this chapter puts requirements alongside syntax.
Read the declaration alone:
template <typename T>
const T& smaller(const T& a, const T& b);Nothing in it says T must be comparable. A reader cannot tell. A caller cannot tell. The compiler cannot tell either — it discovers the requirement when it tries to generate the body for a particular T, which is Chapter 18.6's instantiation.
That is a genuine gap between the interface and the contract, and it is the same category of problem Chapter 10.9 §6 identified for ownership: the information exists, it is not in the signature, and the only place it can live is a comment.
5. So State It
Before C++20, the honest practice is to write it down:
// Requires: T supports `a < b`, usable as a condition.
// Returns: a reference to the smaller argument; a if they compare equal.
template <typename T>
const T& smaller(const T& a, const T& b) { return b < a ? b : a; }Three properties of a good requirement comment:
It names operations, not type categories. "Requires <" is checkable against the body. "Requires a comparable type" is vague and drifts.
It is complete. If the body also calls swap(a, b), that is a second requirement and belongs in the comment.
And it says what the function promises, including the edge case — here, which argument comes back when the two compare equal, because a caller holding a reference may care.
A comment is weak, and Chapter 15.7's callout was blunt about comments being read once while notation is read every time. It is nevertheless better than nothing, and §10 covers the two mechanisms that do better.
6. Multiple Template Parameters
More than one type parameter is ordinary:
template <typename A, typename B>
auto smaller2(const A& a, const B& b) { return b < a ? b : a; }This changes the requirement — now b < a must be valid for a B and an A, which is a cross-type comparison — and it changes the return type, which §7 covers. Chapter 18.3 §5 measures why you would want the two-parameter form.
7. The Return Type Is a Decision
Three choices, and they are not interchangeable.
const T& — return a reference to one of the arguments. No copy, which matters when T is expensive. This is what the frozen signature uses and it is right for smaller.
T — return a copy. Safe in every case and wasteful when T is large.
auto — let the compiler decide, which is necessary for §6's two-parameter form because the result type is neither A nor B:
decltype(smaller2(1, 2.0)) is double? 1 is a reference? 0auto deduced double and returned by value. It had to: the common type of an int and a double is a double, and there is no double object among the arguments to refer to. Chapter 18.3 §6 returns to this.
And the hazard with const T&, which Chapter 15.3 §7 measured in its general form: a reference return is only safe because both arguments are references to objects the caller owns. Change the parameters to by-value and the same return type dangles:
template <typename T> const T& broken(T a, T b) { return b < a ? b : a; } // returns a dead parameterChapter 15.3 §7 measured that producing -Wreturn-stack-address, a correct-looking answer at run time, and an ASan report only with a non-default option.
8. When a Requirement Is Not Met
struct NoLess { int v; };
smaller(a, b); // no operator<diagnostic lines: 7 errors: 1
noless.cpp:1:75: error: invalid operands to binary expression ('const NoLess' and 'const NoLess')
noless.cpp:3:40: note: in instantiation of function template specialization 'smaller<NoLess>' requested hereTwo things worth noticing, and the second corrects a reputation.
The error is reported inside the template — line 1, the body — while the mistake was at the call on line 3. The note: in instantiation of … requested here is the bridge, and it is the line that tells you which call was wrong. Chapter 18.6 explains why the diagnostic arrives this way round.
And the error is seven lines long. Templates have a reputation for enormous diagnostics, and Chapter 18.7 measures one at 370 lines — but that one comes from std::sort, whose internals are many layers deep. A simple template's failure is short and readable. The length comes from depth, not from templates as such, which is worth knowing before you learn to dread them.
9. A Plain Function Beats the Template
The practical companion to §3: when a template's requirement is satisfiable but wrong for a particular type, you do not have to abandon the template. Add an ordinary function.
smaller works on const char* — but b < a on two pointers compares addresses, not text, which is almost never what the caller meant. So:
template <typename T> const T& smaller(const T& a, const T& b) { return b < a ? b : a; }
const char* smaller(const char* a, const char* b) { return std::strcmp(b,a) < 0 ? b : a; }A. exact match for BOTH (const char*):
plain : apple
B. only the template matches (int):
template : 3
C. forcing the template with explicit <>:
template : pear (compared the POINTERS, not the text)When both match equally well, the non-template wins. smaller("pear","apple") reached the plain function and returned apple, the correct text comparison. smaller(3,7) reached the template, because no plain overload matched. And C shows what the template would have done: smaller<const char*> forced the generic path and returned "pear", having compared two addresses.
The rule is Chapter 6.7's overload resolution with one addition: a non-template function is preferred over a template specialisation when neither requires a worse conversion. That makes an ordinary overload the simplest way to override a template's behaviour for one type.
It is also, as Chapter 18.8 §6 measures, usually the better way — full specialisation of a function template looks like the obvious tool for this job and interacts with overload resolution in a way that surprises people.
10. What C++ Offers Instead of a Comment
Two mechanisms, one available now and one not.
static_assert with a trait states the requirement in code and produces a message you wrote. Chapter 18.10 §5 measures the same failure as §8 reduced from 370 lines to 7 with one static_assert, and the error says what you told it to.
And C++20's concepts put the requirement in the declaration, where §4 wanted it:
template <std::totally_ordered T> // C++20, not available here
const T& smaller(const T& a, const T& b);That is the real fix, and it is outside this curriculum's C++17 baseline (Chapter 1.3). Knowing it exists matters for reading modern code; the C++17 answer is a comment plus, where it is worth the lines, a static_assert.
11. What to Actually Do
Write template <typename T> and prefer typename to class.
Take parameters by const T& unless you need a copy.
Read the body to find the requirements — they are the operations it performs, nothing more.
State them in a comment, naming operations rather than type categories, and keep it complete.
Return const T& only when the result is one of the arguments and the arguments are references.
Use auto for a multi-parameter template's return type.
Add a static_assert where the template is part of an interface others call.
And do not expect every template failure to be enormous — a simple one was seven lines.
12. Mistakes Worth Making Once
Believing a template works for "any type". It works for types supporting the operations in its body.
Assuming more requirements than the body has. Measured: a type with no default constructor, no copy and no assignment worked fine.
Assuming operator< must return bool. Measured: int worked, via contextual conversion.
Expecting the declaration to state the requirement. It cannot, before C++20.
Writing a vague requirement comment. "Comparable" is not checkable; "a < b" is.
Returning const T& from by-value parameters. Chapter 15.3 §7 — a dangling reference that prints the right answer.
Returning const A& from a two-type template. The result may be neither type; use auto.
Dreading all template errors. The simple case was 7 lines; depth is what makes them long.
13. Interview Relevance
"Write a function template." template <typename T> const T& smaller(const T& a, const T& b). The strong answer explains each part, including why const T&.
"What does it require of T?" Only the operations in the body — here b < a usable as a condition. Saying "no more and no less" is the distinguishing answer.
"Does T need to be copyable?" Not for this template. Measured: a type with no copy constructor worked.
"Where is the requirement written?" Nowhere the compiler checks, before C++20 — which is why a comment plus a static_assert is the C++17 practice.
"typename or class?" Identical here; typename reads better because T need not be a class.
"When would you return auto?" With more than one type parameter, where the result may be neither — measured deducing double by value for an int/double pair.
"Are template errors always huge?" No — measured 7 lines for a simple template. Length comes from instantiation depth.
14. What You Should Be Able to Say Now
template <typename T>introduces a template parameter list, andtypenameandclassmean the same thing there.- Take parameters by
const T&— Chapter 6.5's rule, which matters more generically. - The compiler deduces
Tfrom the arguments — Chapter 18.3. - Chapter 18.1 §5 measured the instantiation as byte-identical to a hand-written function.
- A template's requirements are the operations its body performs, no more and no less.
- Measured: a type with only a friend
operator<— no default constructor, no copy, no assignment — worked. - Measured: an
operator<returningintworked, via contextual conversion to a condition. - The requirement is invisible in the declaration, so a reader must open the body.
- State it in a comment, naming operations, completely, including what the function promises on ties.
- A multi-parameter template changes the requirement and the return type.
- Measured:
autodeduceddouble, by value, for anint/doublepair — there was no object of the common type to refer to. const T&is only safe when the arguments are references — Chapter 15.3 §7 measured the by-value version dangling.- Measured: a missing operation gave 7 diagnostic lines, 1 error, reported inside the template with a
note: in instantiation of … requested herenaming the call. - Template errors are long because of depth, not because of templates — Chapter 18.7's 370-line case is
std::sort's. static_assertis the C++17 way to make a requirement checkable; concepts are the C++20 answer.
15. Exercises
Exercise 1 — write one. A function template returning the larger of two values, with the parameter and return types chosen deliberately.
Exercise 2 — list the requirements. For a template that returns (a + b) / 2, list every operation T must support.
Exercise 3 — the minimal type. Define a type that satisfies your larger template and nothing more — no default constructor, no copy.
Exercise 4 — the comment. Write the requirement comment for larger, naming operations and stating the tie behaviour.
Exercise 5 — the return type. Say which of T, const T& and auto you would return for: smaller, midpoint, and a two-type smaller2.
Exercise 6 — the dangling version. Change the parameters to by-value and say what the const T& return does, with the warning you expect.
Exercise 7 — predict the error. Pass a type with no operator<. Say roughly how long the diagnostic is, which line holds the error, and which line names your mistake.
Exercise 8 — make it checkable. Add a static_assert stating the requirement, and say what it changes about the failure.
16. Summary
template <typename T>plus an ordinary function declaration, withconst T¶meters by default.- A template's requirements are the operations in its body — measured, a type with only a friend
operator<and no copy, assignment or default constructor worked. - Measured:
operator<returningintalso worked — the requirement is usable as a condition. - The requirement is invisible in the declaration, so write it as a comment naming operations, and keep it complete.
- Return
const T&only when the result is one of the arguments and they are references — Chapter 15.3 §7 measured the by-value version dangling. - Measured:
autoon a two-type template deduceddoubleby value, because no argument had the common type. - Measured: a missing operation gave 7 lines, reported in the template, with a note naming the call site.
- Long template errors come from depth — Chapter 18.7's 370-line case is
std::sort's, not a simple template's. static_assertmakes the requirement checkable in C++17; concepts are the C++20 answer.
The syntax is settled and §2 passed over the step that makes it usable in one sentence: the compiler worked T out from the arguments. That deduction has rules, the rules are not what most people assume, and getting them wrong produces code that compiles and does something slightly different from what was intended. The next chapter measures them — a full table of what T becomes for a plain value, a const value, a reference, an rvalue and an array, across four different parameter forms — and three of the twenty results are surprises. By-value parameters silently discard const and references. Arrays decay to pointers through a by-value parameter and keep their type through a reference. And T&& deduces something that is not a type you wrote. It also covers the two cases where deduction simply fails, including one that fails for a reason worth seeing: two arguments of different types for a single T.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
