C++ Module 9 – Templates and concepts
C++ function templates and argument deduction
Write one function for many types, predict the type the compiler deduces from each call, and read the errors when deduction or instantiation fails.
What you will learn
- Write a function template that works for every type with the operations its body uses
- Predict the T that deduction gives for values, references, arrays and string literals
- Trace an instantiation error back to the line of your own code that caused it
- Fix a call whose arguments deduce conflicting types
- Choose between a template line, auto parameters and the standard library's std::max
Before you start
What you’ll learn · Before you start
What you will learn
- Write a function template that works for every type with the operations its body uses
- Predict the T that deduction gives for values, references, arrays and string literals
- Trace an instantiation error back to the line of your own code that caused it
- Fix a call whose arguments deduce conflicting types
- Choose between a template line, auto parameters and the standard library's std::max
Before you start
On this page
Without templates, larger(3, 7), larger(2.5, -1.0) and larger('q', 'k') need three functions with the same
body, one per type, and every new type needs one more copy. A function template: A function written with template parameters, such as template <typename T> T larger(T a, T b), from which the compiler writes a separate function for each list of template arguments that a call needs.
keeps the body once: from template <typename T> T larger(T a, T b) the compiler writes larger<int>,
larger<double> and larger<char> itself, working out T from the arguments of each call.
You will see the functions the compiler writes, predict the T it deduces from references, arrays and string
literals, settle calls such as larger(1, 2.5) whose arguments propose two types, and read the errors both compilers
print when a type lacks an operation the body uses. Every example ran with Apple clang 21 and GCC 16.
One template replaces an overload for every type
The line template <typename T> introduces a template parameter: A name declared in a template's parameter list, such as T in template <typename T> or N in template <std::size_t N>, that stands for a type or a value chosen each time the template is used. named T, and
the rest is an ordinary function that uses T wherever a type would go. class means the same as typename in that
line; this track writes typename. Here is the template with five calls of it, run on both compilers:
// One function template, called with four types: the compiler writes a larger() for each type.
#include <compare>
#include <print>
template <typename T>
T larger(T a, T b) {
return a > b ? a : b;
}
struct Version {
int major;
int minor;
auto operator<=>(const Version&) const = default; // gives Version the > that larger() needs
};
int main() {
std::println("{}", larger(3, 7));
std::println("{}", larger(2.5, -1.0));
std::println("{}", larger('q', 'k'));
std::println("{}", larger<double>(4, 6.5));
Version newer = larger(Version{1, 10}, Version{1, 4});
std::println("{}.{}", newer.major, newer.minor);
} Output
7 2.5 q 6.5 1.10
Recorded with Apple clang version 21.0.0 (clang-2100.3.34.2) on macOS 26 arm64. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined larger.cpp -o larger && ./larger
The template works for any type that supports what its body does with T: copying a value and comparing two values
with >. Numbers and characters have > built in, and Version gets it from its defaulted operator<=>, the
three-way comparison: The operator <=> (C++20): one comparison that says whether a value is less than, equal to or greater than another, or unordered with it. Defaulted, it compares the members in declaration order, and the compiler rewrites <, <=, > and >= in terms of it. More in “C++ structs, aggregates and designated initialisers”, which compares major first and minor second, so
version 1.10 beats version 1.4. The fourth call, larger<double>(4, 6.5), names the
template argument: The type or value that fills a template parameter in one use of a template, such as int in larger<int>. It is written in angle brackets after the template's name, or deduced from the arguments of a call. in angle brackets instead of letting the compiler work it out;
a later section shows when that is necessary.
Checkpoint
Check: Writing a function template
The compiler writes one function per type
A template is not compiled on its own. Each time a call needs it with a new type, the compiler
instantiation: The compiler writing a real function or class from a template for particular template arguments, such as larger<int> from larger, the first time a use needs it. Each instantiation is compiled as a function or class of its own. it: it puts the type in place of T and compiles the result as a
separate function. Step through the five calls of larger.cpp and watch which call makes a new function and which
one reuses a function made before:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The first call. larger(3, 7) passes two ints, so T is int. No larger<int> exists yet, so the compiler writes one from the template, with int in place of T. The program printed 7.
- A second type. larger(2.5, -1.0) passes two doubles. That is a new T, so a second function is written, larger<double>, compiled on its own. It printed 2.5.
- A third type. larger('q', 'k') passes two chars, and char has > built in, so larger<char> is written too. 'q' comes after 'k', and the program printed q.
- The type named. larger<double>(4, 6.5) names T itself, so nothing is deduced. larger<double> exists already, and the call reuses it: 4 converts to 4.0 like any argument, and the program printed 6.5.
- A class type. Two Version objects make T = Version. The defaulted operator<=> gives Version the > that the body needs, comparing major, then minor, so 1.10 is the larger and was printed.
- Five calls, four functions. One function for each different T, and none for the call that reused one. nm -C listed exactly these four in larger.o, with the same names on Apple clang and GCC.
Five calls produced four functions, because larger<double>(4, 6.5) reuses the larger<double> that the call with
two doubles needed already. The object file shows them. nm -C lists the functions an object file defines, with
their C++ names:
from session import run
# Compile only (-c), then list the functions each object file defines whose name contains "larger".
run("""
clang++ -std=c++23 -c larger.cpp -o larger.o && nm -C -j larger.o | grep larger
g++-16 -std=c++23 -c larger.cpp -o larger-gcc.o && nm -C -j larger-gcc.o | grep larger
""") Output
$ clang++ -std=c++23 -c larger.cpp -o larger.o && nm -C -j larger.o | grep larger Version larger<Version>(Version, Version) char larger<char>(char, char) double larger<double>(double, double) int larger<int>(int, int) $ g++-16 -std=c++23 -c larger.cpp -o larger-gcc.o && nm -C -j larger-gcc.o | grep larger Version larger<Version>(Version, Version) char larger<char>(char, char) double larger<double>(double, double) int larger<int>(int, int)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 instances_session.py
Both compilers agree, and the names carry the template argument: larger<int> and larger<char> are as distinct as
two functions with different names. Each instantiation is compiled and optimised on its own, which is why heavy use of
templates costs build time and program size, a cost a later lesson of this module measures. The diagram sums up the
path of every call: deduction first, then one compiled function or an error.
From a call to a compiled function, or to a deduction error
Text description of the diagram
The diagram follows one call to the function template larger, declared as template <typename T> T larger(T a, T b).
- The call, for example larger(x, y), starts template argument deduction: each argument proposes a type for T.
- If every argument proposes the same type, as larger(3, 7) does with int, the compiler writes the function larger<int> the first time it is needed. Every later call with two ints reuses it.
- If the arguments propose different types, as larger(1, 2.5) does with int and double, deduction fails with the error "deduced conflicting types for parameter T". Naming the type yourself, larger<double>(1, 2.5), fixes the call.
Key idea
A template is a recipe, not a function: each different T that a call needs gets its own compiled function, and
later calls with the same T reuse it.
Checkpoint
Check: One compiled function per type
Errors appear when a template is instantiated
Nothing in the declaration of larger says that T needs a > operator; the requirement is hidden in the body. So
when a type lacks it, the error is reported inside the template, at the line that uses it, and only when a call makes
the compiler instantiate the template for that type:
// larger() asks for a > b. Point has no > operator, so larger<Point> cannot be compiled.
template <typename T>
T larger(T a, T b) {
return a > b ? a : b;
}
struct Point {
int x;
int y;
};
int main() {
Point p{1, 2};
Point q{3, 4};
Point r = larger(p, q);
return r.x;
} from session import run
run("""
clang++ -std=c++23 -c no_greater.cpp
g++-16 -std=c++23 -c no_greater.cpp
""") Output
$ clang++ -std=c++23 -c no_greater.cpp
no_greater.cpp:4:14: error: invalid operands to binary expression ('Point' and 'Point')
4 | return a > b ? a : b;
| ~ ^ ~
no_greater.cpp:15:15: note: in instantiation of function template specialization 'larger<Point>' requested here
15 | Point r = larger(p, q);
| ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -c no_greater.cpp
no_greater.cpp: In instantiation of 'T larger(T, T) [with T = Point]':
no_greater.cpp:15:21: required from here
15 | Point r = larger(p, q);
| ~~~~~~^~~~~~
no_greater.cpp:4:14: error: no match for 'operator>' (operand types are 'Point' and 'Point')
4 | return a > b ? a : b;
| ~~^~~
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 no_greater_session.py
Both errors name line 4, inside the template, and both lead back to line 15 of main. Step through what the
compiler did, from the call to the error and back along the note:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- A call that looks fine. Line 15 of main calls larger(p, q) with two Points. The declaration template <typename T> T larger(T a, T b) asks nothing of T, so nothing is wrong with the call as far as the declaration can tell.
- T is Point. Both arguments are Point, so deduction agrees on T = Point without any trouble.
- The body is compiled. To call larger<Point>, the compiler needs it, so it compiles the body of the template with Point in place of T. Only now is the body checked for this type.
- No > for Point. Line 4 compares a > b. Point has no operator>, so the line cannot compile. Clang says invalid operands to binary expression ('Point' and 'Point'); GCC says no match for 'operator>'.
- Follow the note back. Under the error, Clang's note in instantiation of ... 'larger<Point>' requested here, and GCC's required from here, point at line 15: the call that asked for the function.
- The line you can change. Line 4 is right for every type that has a >. The fix is at the call, or in Point: give Point the operation, or call something that does not need it. In a long chain of notes, look for the last frame in a file you own.
Read such an error from the instantiation note back to your own code. Clang adds in instantiation of function template specialization 'larger<Point>' requested here and GCC begins with In instantiation of ... [with T = Point]
followed by required from here; both then show line 15 of main, which is the line you can change. In real
projects the chain between your call and the failing line can be dozens of frames long, through library templates,
and the frame to look for is the last one in a file you own. C++20 concepts, covered later in this module, let a
template state its requirements in its declaration, so that the compiler can reject a type at the call with a short
message.
Editing the template where the error points
The error names line 4, but a > b is right for every type that has a >. Change the call on line 15, or give
Point the operation, for example a defaulted operator<=> as Version has.
Checkpoint
Check: Errors at instantiation
By value, deduction drops const and decays arrays
type deduction: The compiler working out a type from an initializer or an argument instead of reading it from the declaration, as it does for auto, decltype(auto) and the parameters of function templates. More in “C++ auto, decltype and type aliases” compares the type of each argument with the type of the parameter it is
passed to, and works out a T that makes them match. Two rules decide most cases:
- When the parameter is taken by value (
T), the argument’s references and its top-level const: A const that applies to an object itself, as in const int or int* const, rather than to what a pointer or a reference leads to. auto drops it, because the new variable is a separate object. More in “C++ auto, decltype and type aliases” are dropped, because the function gets its own copy anyway, and an array or a function array decay: The everyday name for the array-to-pointer conversion: a C array used as a value, or passed to a function, turns into a pointer to its first element, and its size is lost. A parameter declared as int a[5] is really int* a. More in “std::array, C arrays and array decay” to a pointer. - When the parameter is a reference such as
const T&, nothing decays: an array stays an array, and only theconstalready written in the parameter is matched away.
The program below prints the name of each instantiated function. std::source_location, standard since C++20, gives
it to the program, and both compilers write the deduced T into that name:
// What does the compiler deduce for T? Each function prints its own name, which the compiler writes with T in it.
#include <print>
#include <source_location>
template <typename T>
void take_copy(T) {
std::println("{}", std::source_location::current().function_name());
}
template <typename T>
void take_ref(const T&) {
std::println("{}", std::source_location::current().function_name());
}
int main() {
const int limit = 10;
int count = 3;
int& alias = count;
int marks[4] = {72, 91, 64, 85};
take_copy(limit); // a const int
take_copy(alias); // a reference to int
take_copy(marks); // an array of 4 ints
take_copy("pear"); // a string literal, const char[5]
take_ref(limit);
take_ref(marks);
take_ref("pear");
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra deduce.cpp -o deduce && ./deduce
g++-16 -std=c++23 -Wall -Wextra deduce.cpp -o deduce-gcc && ./deduce-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra deduce.cpp -o deduce && ./deduce void take_copy(T) [T = int] void take_copy(T) [T = int] void take_copy(T) [T = int *] void take_copy(T) [T = const char *] void take_ref(const T &) [T = int] void take_ref(const T &) [T = int[4]] void take_ref(const T &) [T = char[5]] $ g++-16 -std=c++23 -Wall -Wextra deduce.cpp -o deduce-gcc && ./deduce-gcc void take_copy(T) [with T = int] void take_copy(T) [with T = int] void take_copy(T) [with T = int*] void take_copy(T) [with T = const char*] void take_ref(const T&) [with T = int] void take_ref(const T&) [with T = int [4]] void take_ref(const T&) [with T = char [5]]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 deduce_session.py
The two compilers spell the names differently ([T = int *] and [with T = int*]) and agree on every T. Step
through the seven calls and watch each rule act on its argument, or leave it alone:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- take_copy(limit). limit is a const int. take_copy takes T by value, so it gets a copy, and the const of the original does not matter to a copy: T = int.
- take_copy(alias). alias is an int& bound to count. An expression never has a reference type: alias is read as the int it names, and the copy is a plain int. T = int.
- take_copy(marks). marks is an array of 4 ints. An array cannot be copied into a parameter, so it decays to a pointer to its first element: T = int*, and the 4 is lost.
- take_copy("pear"). A string literal is an array of const char: 4 letters and the final zero, const char[5]. It decays like marks, and the const belongs to the characters, so it stays: T = const char*.
- take_ref(limit). take_ref takes const T&. The parameter already says const, so const int matches const T with T = int. Nothing is copied: the reference binds to limit itself.
- take_ref(marks). A reference can bind to a whole array, so marks does not decay: T = int[4], and the function knows the size from its type.
- take_ref("pear"). The literal keeps its array type too. Its const is matched by the const in const T&, so T is what is left: char[5].
const int limit and the reference alias both give plain int to take_copy. The array marks becomes int*,
and the four-element size is lost, while take_ref(marks) keeps it as int[4]. The
string literal: Text in double quotes in the source, such as "pune": an array of const char holding the characters and a terminating '\0', made before the program starts and kept for its whole run. Writing into one is undefined behaviour. More in “C-style strings and why std::string replaced them” "pear" has the type const char[5], four letters and the
terminating zero; passed by value it becomes const char*, and passed to take_ref it gives T = char[5], because
the const is already part of const T&.
Key idea
A by-value parameter deduces the type of a copy: no reference, no top-level const, and a pointer for an array. A
const T& parameter keeps arrays whole.
Checkpoint
Check: How T is deduced
Two string literals compare as addresses
The decay rule turns two string literals into two pointers, and comparing two pointers with > compares where the
text is stored, not the text. Nothing warns about it:
literals.cpp
// Two string literals passed to larger(): T is deduced as const char*, so > compares two addresses.
#include <print>
#include <string>
using namespace std::string_literals; // for the s suffix: "pear"s is a std::string
template <typename T>
T larger(T a, T b) {
return a > b ? a : b;
}
int main() {
std::println("larger(\"pear\", \"apple\") = {}", larger("pear", "apple"));
std::println("larger(\"apple\", \"pear\") = {}", larger("apple", "pear"));
std::println("larger(\"pear\"s, \"apple\"s) = {}", larger("pear"s, "apple"s));
} literals_ref.cpp
// The same call with const T& parameters: the arrays do not decay, and their types differ.
template <typename T>
const T& larger_ref(const T& a, const T& b) {
return a > b ? a : b;
}
int main() {
const char* word = larger_ref("pear", "apple");
return word[0] == 'p' ? 0 : 1;
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra literals.cpp -o literals && ./literals
g++-16 -std=c++23 -Wall -Wextra literals.cpp -o literals-gcc && ./literals-gcc
clang++ -std=c++23 -c literals_ref.cpp
g++-16 -std=c++23 -c literals_ref.cpp
""") Output
$ clang++ -std=c++23 -Wall -Wextra literals.cpp -o literals && ./literals
larger("pear", "apple") = apple
larger("apple", "pear") = apple
larger("pear"s, "apple"s) = pear
$ g++-16 -std=c++23 -Wall -Wextra literals.cpp -o literals-gcc && ./literals-gcc
larger("pear", "apple") = pear
larger("apple", "pear") = pear
larger("pear"s, "apple"s) = pear
$ clang++ -std=c++23 -c literals_ref.cpp
literals_ref.cpp:8:24: error: no matching function for call to 'larger_ref'
8 | const char* word = larger_ref("pear", "apple");
| ^~~~~~~~~~
literals_ref.cpp:3:10: note: candidate template ignored: deduced conflicting types for parameter 'T' ('char[5]' vs. 'char[6]')
3 | const T& larger_ref(const T& a, const T& b) {
| ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -c literals_ref.cpp
literals_ref.cpp: In function 'int main()':
literals_ref.cpp:8:34: error: no matching function for call to 'larger_ref(const char [5], const char [6])'
8 | const char* word = larger_ref("pear", "apple");
| ~~~~~~~~~~^~~~~~~~~~~~~~~~~
• there is 1 candidate
• candidate 1: 'template<class T> const T& larger_ref(const T&, const T&)'
literals_ref.cpp:3:10:
3 | const T& larger_ref(const T& a, const T& b) {
| ^~~~~~~~~~
• template argument deduction/substitution failed:
• deduced conflicting types for parameter 'const T' ('char [5]' and 'char [6]')
literals_ref.cpp:8:34:
8 | const char* word = larger_ref("pear", "apple");
| ~~~~~~~~~~^~~~~~~~~~~~~~~~~
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 literals_session.py
Apple clang says apple is the larger word and GCC says pear, whichever order the arguments come in: each compiler
lays out the two literals in its own way, and the standard does not order the addresses of unrelated objects. With
std::string arguments, written "pear"s thanks to the s suffix of std::string_literals, the comparison reads
the characters and both compilers agree that pear comes after apple. The reference version fails for a different
reason: the literals keep their array types, char[5] and char[6], and those are two different types for one T.
Comparing C strings with > in a template
larger("pear", "apple") compiles, runs and gives each compiler’s own answer. Pass std::string or
std::string_view whenever a template should compare text, as larger("pear"s, "apple"s) does.
Checkpoint
Check: String literals as arguments
When the arguments disagree, name the type
Deduction considers each argument on its own and never converts one to make them agree. That is why
larger(1, 2.5) fails, with the same verdict from both compilers:
from session import run
run("""
clang++ -std=c++23 -c conflict.cpp
g++-16 -std=c++23 -c conflict.cpp
""") Output
$ clang++ -std=c++23 -c conflict.cpp
conflict.cpp:8:16: error: no matching function for call to 'larger'
8 | double d = larger(1, 2.5);
| ^~~~~~
conflict.cpp:3:3: note: candidate template ignored: deduced conflicting types for parameter 'T' ('int' vs. 'double')
3 | T larger(T a, T b) {
| ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -c conflict.cpp
conflict.cpp: In function 'int main()':
conflict.cpp:8:22: error: no matching function for call to 'larger(int, double)'
8 | double d = larger(1, 2.5);
| ~~~~~~^~~~~~~~
• there is 1 candidate
• candidate 1: 'template<class T> T larger(T, T)'
conflict.cpp:3:3:
3 | T larger(T a, T b) {
| ^~~~~~
• template argument deduction/substitution failed:
• deduced conflicting types for parameter 'T' ('int' and 'double')
conflict.cpp:8:22:
8 | double d = larger(1, 2.5);
| ~~~~~~^~~~~~~~
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 conflict_session.py
Step through the deduction for that call, then the two fixes that explicit.cpp below runs:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The first pair. Deduction pairs the first argument with the first parameter. 1 is an int, and the parameter is T a, so this pair proposes T = int.
- The second pair. The second pair is looked at on its own: 2.5 is a double, so it proposes T = double. Neither pair knows what the other proposed.
- No T fits both. The proposals disagree, and deduction never converts an argument to make them agree. No larger can be made: both compilers report deduced conflicting types for parameter 'T' ('int' vs. 'double').
- Name the type. larger<double>(1, 2.5) sets T before the arguments are paired, so there is nothing left to deduce. 1 then converts to 1.0 as in any call, and the program printed 2.5.
- A parameter each. larger_of_two gives each argument its own template parameter, A = int and B = double, so nothing conflicts. Its auto return type is their common type, double: it printed 2.5.
- The price of mixing. Mixed types follow the usual arithmetic conversions. For larger_of_two(-1, 1u) the common type is unsigned int, -1 becomes 4294967295 before > runs, and that is what the program printed.
You can settle such a call in three ways, all in the next program:
- Name the template argument.
larger<double>(1, 2.5)setsTtodoublebefore the call is matched, and theintargument then converts todoublethe ordinary way. - Change an argument:
larger(1.0, 2.5)gives deduction two doubles. - Give each parameter its own template parameter, as
larger_of_two(A a, B b)does. The return typeautois then the common type ofaandb, which is where a trap waits: comparing-1with1uconverts-1to an unsigned value first.
A template parameter that appears only in the return type, such as To in convert, can never be deduced from a call,
because no argument mentions it. List such parameters first, so that a call names only those (convert<int>(3.99)) and the
compiler deduces the rest.
// Three ways to call a template when deduction alone cannot do it.
#include <print>
template <typename T>
T larger(T a, T b) {
return a > b ? a : b;
}
// To appears only in the return type, so no argument can tell the compiler what it is.
template <typename To, typename From>
To convert(From value) {
return static_cast<To>(value);
}
// Two type parameters: each argument gets its own type, and the result has their common type.
template <typename A, typename B>
auto larger_of_two(A a, B b) {
return a > b ? a : b;
}
int main() {
std::println("larger<double>(1, 2.5) = {}", larger<double>(1, 2.5));
std::println("convert<int>(3.99) = {}", convert<int>(3.99));
std::println("larger_of_two(1, 2.5) = {}", larger_of_two(1, 2.5));
std::println("larger_of_two(-1, 1u) = {}", larger_of_two(-1, 1u));
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra explicit.cpp -o explicit && ./explicit
g++-16 -std=c++23 -Wall -Wextra explicit.cpp -o explicit-gcc && ./explicit-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra explicit.cpp -o explicit && ./explicit
explicit.cpp:18:14: warning: comparison of integers of different signs: 'int' and 'unsigned int' [-Wsign-compare]
18 | return a > b ? a : b;
| ~ ^ ~
explicit.cpp:25:49: note: in instantiation of function template specialization 'larger_of_two<int, unsigned int>' requested here
25 | std::println("larger_of_two(-1, 1u) = {}", larger_of_two(-1, 1u));
| ^
1 warning generated.
larger<double>(1, 2.5) = 2.5
convert<int>(3.99) = 3
larger_of_two(1, 2.5) = 2.5
larger_of_two(-1, 1u) = 4294967295
$ g++-16 -std=c++23 -Wall -Wextra explicit.cpp -o explicit-gcc && ./explicit-gcc
explicit.cpp: In instantiation of 'auto larger_of_two(A, B) [with A = int; B = unsigned int]':
explicit.cpp:25:62: required from here
25 | std::println("larger_of_two(-1, 1u) = {}", larger_of_two(-1, 1u));
| ~~~~~~~~~~~~~^~~~~~~~
explicit.cpp:18:14: warning: comparison of integer expressions of different signedness: 'int' and 'unsigned int' [-Wsign-compare]
18 | return a > b ? a : b;
| ~~^~~
larger<double>(1, 2.5) = 2.5
convert<int>(3.99) = 3
larger_of_two(1, 2.5) = 2.5
larger_of_two(-1, 1u) = 4294967295
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 explicit_session.py
larger_of_two(-1, 1u) returns 4,294,967,295. The usual arithmetic conversions: The rules that give both operands of an arithmetic or comparison operator one common type. When a signed and an unsigned integer meet and the unsigned type is at least as wide, the signed value is converted to unsigned, so −1 < v.size() is false. More in “Integer overflow and signed/unsigned conversions” turn -1 into the largest unsigned int before > runs, so the template faithfully returns the
“larger” value. Both compilers warn, from inside the instantiation larger_of_two<int, unsigned int>, which is one
more reason to compile with -Wall -Wextra: warnings, like errors, are reported for each instantiation.
Checkpoint
Check: Settling a deduction conflict
An auto parameter makes a function a template
C++20 lets you skip the template line. A parameter whose type contains auto makes the function an
abbreviated function template: A function with auto in the type of a parameter, such as void show(const auto& x) (C++20). Each auto invents a template parameter of its own, so the function is a function template written without a template line., and each auto invents one template
parameter of its own:
// C++20 abbreviated function templates: an auto parameter makes the function a template.
#include <iostream>
#include <source_location>
#include <string>
struct Fraction {
int num;
int den;
};
std::ostream& operator<<(std::ostream& out, const Fraction& f) {
return out << f.num << '/' << f.den;
}
// The same as template <typename T> void show(const T& value).
void show(const auto& value) {
std::cout << value << " <- " << std::source_location::current().function_name() << '\n';
}
// Each auto is its own template parameter: a and b may have different types.
auto add(auto a, auto b) {
return a + b;
}
int main() {
show(42);
show(std::string("Meera"));
show(Fraction{3, 4});
show<double>(7);
std::cout << add(2, 0.5) << '\n';
std::cout << add(std::string("C++"), "23") << '\n';
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra abbreviated.cpp -o abbreviated && ./abbreviated
g++-16 -std=c++23 -Wall -Wextra abbreviated.cpp -o abbreviated-gcc && ./abbreviated-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra abbreviated.cpp -o abbreviated && ./abbreviated 42 <- void show(const auto &) [value:auto = int] Meera <- void show(const auto &) [value:auto = std::string] 3/4 <- void show(const auto &) [value:auto = Fraction] 7 <- void show(const auto &) [value:auto = double] 2.5 C++23 $ g++-16 -std=c++23 -Wall -Wextra abbreviated.cpp -o abbreviated-gcc && ./abbreviated-gcc 42 <- void show(const auto:11&) [with auto:11 = int] Meera <- void show(const auto:11&) [with auto:11 = std::__cxx11::basic_string<char>] 3/4 <- void show(const auto:11&) [with auto:11 = Fraction] 7 <- void show(const auto:11&) [with auto:11 = double] 2.5 C++23
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 abbreviated_session.py
void show(const auto& value) is exactly template <typename T> void show(const T& value), so everything above
applies to it: deduction follows the same rules, every type gets its own function, and you may still name the type,
show<double>(7). Clang calls the invented parameter value:auto and GCC auto:11; the names differ, the meaning
does not. add(auto a, auto b) has two independent placeholders, so it accepts an int with a double, and a
std::string with a string literal. The same idea gives generic lambda: A lambda with an auto parameter, or a template parameter list since C++20, whose operator() is a template, so one lambda works for every argument type its body supports, as [](auto x) { return x + x; } does. More in “C++ lambdas: captures and generic lambdas”, such as
[](const auto& x) { ... }, which C++14 already had.
Which form to write depends on what the function needs. Answer the three questions in the table for a function of your own:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Form | Names the type | Ties two parameters | Passes as an object |
|---|---|---|---|
| template <typename T> | Good T is a name: std::vector<T> works in the body | Good T a, T b share one T: larger(1, 2.5) was refused | Poor not an object: pass one function, such as larger<int> |
| auto parameters | Mixed no name to use: write decltype(value) instead | Poor each auto is its own type: add(2, 0.5) mixed an int and a double | Poor a function template like the first: pass show<int>, one function |
| A generic lambda | Mixed decltype(x), or since C++20 []<typename T>(const T& x) | Mixed only with the C++20 form: []<typename T>(T a, T b) | Good a lambda is an object: hand it to std::ranges::count_if |
How the answers decide
- “Must the body name the type, for example to make a std::vector of it?” Yes, the body needs the type: adds a caveat to auto parameters and A generic lambda. decltype(value) can name the type, but a named T reads better; a lambda gets one with the C++20 form []<typename T>.
- “Must two parameters always have the same type?” Yes, mixed types are a mistake: rules out auto parameters and adds a caveat to A generic lambda. Each auto invents a parameter of its own, so nothing makes two types agree; a lambda needs the C++20 form []<typename T>(T a, T b).
- “Will you pass it to another function, such as an algorithm?” Yes, as an argument: adds a caveat to template <typename T> and auto parameters. A function template is not an object until one function is chosen: pass larger<int>, or wrap the call in a lambda.
New in C++20
Abbreviated function templates came with C++20’s concepts (paper P1141R2). GCC and Clang support them from version
10, in -std=c++20 mode and later. Choose the long form when you need to name T inside the function, for example in
std::vector<T>, or when two parameters must have the same type.
Checkpoint
Check: auto parameters
std::max is a template that returns a reference
std::max, std::min and std::clamp in <algorithm> are function templates with the same deduction rules as
larger: std::max(1, 2.5) fails in the same way, and std::max<double>(1, 2.5) settles it. std::ranges::max
takes a whole range, and std::max({4, 9, 2}) takes a braced list:
// The standard library's function templates for the same job.
#include <algorithm>
#include <print>
#include <vector>
int main() {
std::vector<int> runs{48, 112, 7, 64};
int count = 3;
std::println("std::max(3, 7) = {}", std::max(3, 7));
std::println("std::max<double>(1, 2.5) = {}", std::max<double>(1, 2.5));
std::println("std::max({{4, 9, 2}}) = {}", std::max({4, 9, 2}));
std::println("std::ranges::max(runs) = {}", std::ranges::max(runs));
auto best = std::max(count, 5); // a copy of the larger value: safe
std::println("best = {}", best);
// std::max returns a reference. Here it refers to the temporary 5, which is destroyed at the semicolon,
// so reading top afterwards would be undefined behaviour. Both compilers warn.
[[maybe_unused]] const int& top = std::max(count, 5);
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra standard_max.cpp -o standard_max && ./standard_max
g++-16 -std=c++23 -Wall -Wextra standard_max.cpp -o standard_max-gcc && ./standard_max-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra standard_max.cpp -o standard_max && ./standard_max
standard_max.cpp:20:55: warning: temporary bound to local reference 'top' will be destroyed at the end of the full-expression [-Wdangling]
20 | [[maybe_unused]] const int& top = std::max(count, 5);
| ^
1 warning generated.
std::max(3, 7) = 7
std::max<double>(1, 2.5) = 2.5
std::max({4, 9, 2}) = 9
std::ranges::max(runs) = 112
best = 5
$ g++-16 -std=c++23 -Wall -Wextra standard_max.cpp -o standard_max-gcc && ./standard_max-gcc
standard_max.cpp: In function 'int main()':
standard_max.cpp:20:33: warning: possibly dangling reference to a temporary [-Wdangling-reference]
20 | [[maybe_unused]] const int& top = std::max(count, 5);
| ^~~
standard_max.cpp:20:55: note: 'const int' temporary created here
20 | [[maybe_unused]] const int& top = std::max(count, 5);
| ^
std::max(3, 7) = 7
std::max<double>(1, 2.5) = 2.5
std::max({4, 9, 2}) = 9
std::ranges::max(runs) = 112
best = 5
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 standard_max_session.py
Unlike larger, std::max returns a reference to one of its arguments, which saves a copy of large values but
can outlive the argument. Step through the last line of the program and watch what top refers to when the
statement ends:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A temporary for 5. std::max takes its arguments as const int&. count is a variable, but 5 is not, so a temporary int holding 5 is made for the call. It lives until the end of the whole statement.
- A reference comes back. std::max compares 3 with 5 and returns a reference to the larger argument, not a copy of it. Here that is the temporary.
- top is bound to it. top is bound to the reference that came back. That does not keep the temporary alive: only a reference bound directly to a temporary extends its life, and this one came through a function.
- The statement ends. At the semicolon the temporary is destroyed, and top refers to an object that no longer exists. Reading it would be undefined behaviour. Clang warned by default (-Wdangling), GCC with -Wextra (-Wdangling-reference).
- Keep a copy instead. auto best = std::max(count, 5); copies the larger value before the semicolon, so best is an int of its own, and the program printed best = 5.
In const int& top = std::max(count, 5); the reference refers to the temporary object: An unnamed object made while an expression is evaluated, such as the std::string that city + ' Central' produces. It is destroyed at the end of the full expression that made it, unless a reference bound to it keeps it alive. More in “std::string and std::string_view”
5, which is destroyed at the end of the statement, so top is a dangling reference: A reference whose object has already been destroyed, such as one returned from a function to its own local variable. Reading or writing through it is undefined behaviour, even when the program happens to print the right value. More in “C++ references: lifetime extension and dangling”. Clang flags it by default (-Wdangling), GCC once -Wextra turns on -Wdangling-reference. Keep
the result by value, auto best = std::max(count, 5);.
Key idea
Reach for std::max, std::min and std::clamp before writing your own, and keep their results by value: they
return a reference to an argument, which may be a temporary.
Checkpoint
Check: std::max and the reference it returns
Practise: clamp_to and count_matching
generic.h declares two function templates for you to write. clamp_to(value, low, high) limits a value to a range,
and count_matching(range, pred) counts the elements of any range for which a predicate returns true. The sample
tests use them with ints, doubles, chars, strings, a std::vector, a std::array and a plain array.
C header · generic.h
// Starter code for "Your turn": write clamp_to and count_matching.
#pragma once
#include <cstddef>
// low if value is below low, high if value is above high, otherwise value itself.
// The caller makes sure that low <= high.
template <typename T>
T clamp_to(T value, T low, T high) {
// TODO
(void)value;
(void)low;
(void)high;
return T{};
}
// How many elements of range make pred(element) return true. range is anything a range-for loop can walk:
// a std::vector, a std::string, a std::array, a plain array.
template <typename Range, typename Pred>
std::size_t count_matching(const Range& range, Pred pred) {
// TODO
(void)range;
(void)pred;
return 0;
} C++ · generic_tests.cpp
// Sample tests for generic.h. Build: clang++ -std=c++23 -Wall -Wextra generic_tests.cpp -o generic_tests
#include <array>
#include <print>
#include <string>
#include <vector>
#include "generic.h"
int checks = 0;
int failures = 0;
void check(bool ok, const char* what) {
++checks;
if (!ok) {
++failures;
std::println("FAIL {}", what);
}
}
int main() {
check(clamp_to(42, 0, 100) == 42, "an int inside the range stays as it is");
check(clamp_to(-3, 1, 100) == 1, "an int below the range becomes low");
check(clamp_to(250, 0, 100) == 100, "an int above the range becomes high");
check(clamp_to(7.5, 0.0, 5.0) == 5.0, "doubles work too");
check(clamp_to('q', 'a', 'f') == 'f', "and chars");
check(clamp_to(std::string("mango"), std::string("banana"), std::string("cherry")) == "cherry",
"strings compare in dictionary order");
std::vector<int> scores{35, 72, 18, 90, 41};
check(count_matching(scores, [](int s) { return s >= 40; }) == 3, "three scores of at least 40");
check(count_matching(std::string("banana"), [](char c) { return c == 'a'; }) == 3, "three a's in banana");
std::array<double, 3> readings{0.5, -1.25, 2.0};
check(count_matching(readings, [](double r) { return r < 0; }) == 1, "one negative reading");
int plain[] = {2, 4, 6, 7};
check(count_matching(plain, [](int n) { return n % 2 == 0; }) == 3, "a plain array of four ints");
check(count_matching(std::vector<int>{}, [](int) { return true; }) == 0, "an empty range has no matches");
std::println("{} of {} checks passed", checks - failures, checks);
return failures == 0 ? 0 : 1;
} from session import run
# The sample tests, built against the starter code before any TODO is done.
run("""
clang++ -std=c++23 -Wall -Wextra generic_tests.cpp -o generic_tests && ./generic_tests
g++-16 -std=c++23 -Wall -Wextra generic_tests.cpp -o generic_tests-gcc && ./generic_tests-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra generic_tests.cpp -o generic_tests && ./generic_tests FAIL an int inside the range stays as it is FAIL an int below the range becomes low FAIL an int above the range becomes high FAIL doubles work too FAIL and chars FAIL strings compare in dictionary order FAIL three scores of at least 40 FAIL three a's in banana FAIL one negative reading FAIL a plain array of four ints 1 of 11 checks passed [exit status 1] $ g++-16 -std=c++23 -Wall -Wextra generic_tests.cpp -o generic_tests-gcc && ./generic_tests-gcc FAIL an int inside the range stays as it is FAIL an int below the range becomes low FAIL an int above the range becomes high FAIL doubles work too FAIL and chars FAIL strings compare in dictionary order FAIL three scores of at least 40 FAIL three a's in banana FAIL one negative reading FAIL a plain array of four ints 1 of 11 checks passed [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 generic_session.py
Only the empty-range check passes before you write anything. Once both templates are right, the program reports 11 of
11 checks and exits with status 0 on both compilers. Write clamp_to with < only, so that it needs nothing more
from T than the standard’s std::clamp does, and let count_matching walk the range with a range-based for over
const auto&. Then add the call clamp_to(2.5, 0, 10) to the tests and predict the error before you compile it.
Then test what you can predict without a compiler: a deduced T, a count of instantiations, the line to fix. The
idea you are least sure of comes up first, and each choice carries a note on the rule it applies:
Practice that adapts to you
Practise this lesson
16 questions about 8 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · Why does larger(1, 2.5) not compile when larger is declared template <typename T> T larger(T a, T b), and how can you fix the call?
Template argument deduction looks at each argument separately: 1 makes T an int and 2.5 makes it a double.
One T cannot be both, and deduction never converts an argument to make the two agree, so no function larger can
be made for the call and the compiler reports deduced conflicting types. There are three fixes. Name the argument,
larger<double>(1, 2.5), after which 1 converts to double as in any ordinary call; pass arguments of one type,
larger(1.0, 2.5); or declare the template with two type parameters, which accepts mixed types but returns their
common type, and mixing signed and unsigned integers then gives surprising results such as -1 comparing greater
than 1u.
An interviewer usually tests the answer on calls you have not seen. Decide these two:
Checkpoint
Interview round: two follow-up questions
Two harder questions take templates into code a team owns: a comparison that differs between compilers, and an error message hundreds of lines long:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A teammate's template <typename T> T larger(T a, T b) returns "apple" for larger("pear", "apple") with Apple clang and "pear" with GCC. Explain what is deduced and compared, then change the code so that this call is either correct or rejected at compile time.
-
Mid to senior
After a teammate adds one call to a function template from a utility header, the build fails with hundreds of lines that point deep inside the header instead of at their code. How do you find the real cause, and what can the template's author do so that the next mistake gives a short error?
Key takeaways
template <typename T>makes a function template; each distinctTa call needs gets its own compiled function, whichnm -Clists aslarger<int>,larger<double>and so on.- A template’s requirements are hidden in its body, so errors appear at instantiation: follow the note to the first line in your own code.
- By-value parameters drop references and top-level
constand turn arrays and string literals into pointers;const T¶meters keep arrays as arrays. - Two string literals passed by value compare as addresses, with answers that differ between compilers; pass
std::stringorstd::string_viewto compare text. - Deduction matches each argument against its parameter on its own and never converts; arguments that propose
different types make the call fail until you name the type, as in
larger<double>(1, 2.5). void f(const auto& x)is an abbreviated function template (C++20); preferstd::max,std::clampandstd::ranges::max, and keep their results by value.
Terms in this lesson
- abbreviated function template
- A function with auto in the type of a parameter, such as void show(const auto& x) (C++20). Each auto invents a template parameter of its own, so the function is a function template written without a template line.
- function template
- A function written with template parameters, such as template <typename T> T larger(T a, T b), from which the compiler writes a separate function for each list of template arguments that a call needs.
- instantiation
- The compiler writing a real function or class from a template for particular template arguments, such as larger<int> from larger, the first time a use needs it. Each instantiation is compiled as a function or class of its own.
- template argument
- The type or value that fills a template parameter in one use of a template, such as int in larger<int>. It is written in angle brackets after the template's name, or deduced from the arguments of a call.
- template parameter
- A name declared in a template's parameter list, such as T in template <typename T> or N in template <std::size_t N>, that stands for a type or a value chosen each time the template is used.
References
- Working Draft, Programming Languages: C++ (N4950), 13.10.3.2 Deducing template arguments from a function call, 13.9.2 Implicit instantiation, 9.3.4.6 Functions, 6.7.7 Temporary objects (ISO/IEC JTC1/SC22/WG21)
- P1141R2: Yet another approach for constrained declarations (ISO/IEC JTC1/SC22/WG21)
- C++ Standards Support in GCC (GNU Project)
- C++ Support in Clang (LLVM Project)
- Options Controlling C++ Dialect (Using the GNU Compiler Collection) (GNU Project)
- Diagnostic flags in Clang (LLVM Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress