Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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 template, five calls C++ · larger.cpp
// 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

  1. Question 1 of 2 In template <typename T> T larger(T a, T b), what does typename T declare?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A name for a type that is chosen each time the template is used

    The template line introduces the parameter T, a stand-in for a type. Each use of the template fills it with a real type, given in angle brackets or worked out from the call, and the compiler writes a function for that type.

  2. Question 2 of 2 struct Score { int points; }; has no operators. Which member lets larger(Score{3}, Score{7}) compile?

    Choose one answer.

    A hint

    Look at what the body of larger does with a and b.

    Show the answer: Question 2 of 2

    Answer: auto operator<=>(const Score&) const = default;

    larger copies its arguments and compares them with >. Score can already be copied, so the only thing missing is the comparison, and a defaulted operator<=> provides <, <=, > and >= for it.

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:

Interactive · step by step Five calls of larger, and the functions the compiler writes Step through main of larger.cpp one call at a time. The left column is the call; the right column is the function the compiler writes for it, or finds already written.
Five calls of larger, and the functions the compiler writes, step 6 of 6. Showing larger(3, 7), larger(2.5, -1.0), larger('q', 'k'), larger<double>(4, 6.5), larger of two Versions, larger<int> (written now: printed 7), larger<double> (used twice: 2.5, then 6.5), larger<char> (written now: printed q), larger<Version> (written now: printed 1.10). In focus: larger<int>, larger<double>, larger<char>, larger<Version>.T = intT = doubleT = charreuses itT = Versionlarger(3, 7)two intslarger(2.5, -1.0)two doubleslarger('q', 'k')two charslarger<double>(4, 6.5)T named in bracketslarger of two Versions{1, 10} and {1, 4}larger<int>written now: printed 7larger<double>used twice: 2.5, then 6.5larger<char>written now: printed qlarger<Version>written now: printed 1.10

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  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.
  6. 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:

The functions inside larger.o Python · instances_session.py
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.

A call to larger(x, y) deduces T from each argument: one agreed type gives one compiled function, two different types give an error.template <typename T>T larger(T a, T b)A call such as larger(x, y)Deduction: each argumentproposes a type for TEvery argumentproposes the same Tlarger(3, 7):T = intThe argumentspropose different typeslarger(1, 2.5):int or double?The compiler writeslarger<int> once, whenfirst needed; latercalls reuse iterror: deducedconflicting types for Tfix: name the type,larger<double>(1, 2.5)

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).

  1. The call, for example larger(x, y), starts template argument deduction: each argument proposes a type for T.
  2. 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.
  3. 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

  1. Question 1 of 2 A program calls larger(1, 2), larger(3, 4), larger(1.5, 2.5) and larger<int>(7, 8). How many functions named larger does its object file define?

    Type a number.

    A hint

    Count the different types T takes, not the calls.

    Show the answer: Question 1 of 2

    Answer: 2 functions

    T is int for three of the calls (twice deduced, once named) and double for one, so the compiler writes larger<int> and larger<double>. Every later call with the same T reuses the function written for the first one.

  2. Question 2 of 2 When does the compiler write the function larger<char>?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: While it compiles the file whose call first needs it with char

    Instantiation happens during compilation, the first time a call needs the template with a particular type. The result is a separate function in the object file, which nm -C lists with its template argument.

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:

no_greater.cpp C++ · no_greater.cpp
// 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;
}
larger() with a type that has no > operator Python · no_greater_session.py
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:

Interactive · stage by stage From the call on line 15 to the error on line 4, and back Step through what the compiler does with larger(p, q). The error is found inside the template; the note under it leads back to the call.
From the call on line 15 to the error on line 4, and back, step 6 of 6. Showing The call, line 15 (fix here, or give Point >), Deduce T (T = Point: no conflict), Write larger<Point> (compiling larger<Point>), Line 4: a > b (no operator> for Point), The error and its note (error at 4, note at 15). In focus: The call, line 15.requested hereThe call, line 15fix here, or give Point >Deduce TT = Point: no conflictWrite larger<Point>compiling larger<Point>Line 4: a > bno operator> for PointThe error and its noteerror at 4, note at 15

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. T is Point. Both arguments are Point, so deduction agrees on T = Point without any trouble.
  3. 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.
  4. 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>'.
  5. 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.
  6. 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

  1. Question 1 of 2 Both compilers report the error of no_greater.cpp at line 4, return a > b ? a : b;, inside larger. What do you change?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The call on line 15, or Point, by giving it a comparison

    An instantiation error is found inside the template but caused by the type a call passes. Follow the note, "requested here" or "required from here", to the line in your own code, and change the call or the type.

  2. Question 2 of 2 A header defines larger, and no file calls it with Point. Does a program that includes the header and declares Point compile?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Yes, because the body is checked for a type only when a call instantiates it with that type

    Instantiation on demand is why one template can serve many types: a body that cannot work for Point causes no trouble until a call asks for larger<Point>, and then the error appears at that 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 the const already 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:

deduce.cpp C++ · deduce.cpp
// 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");
}
The T deduced for four kinds of argument Python · deduce_session.py
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:

Interactive · stage by stage The T that deduction finds for the seven calls of deduce.cpp Step through one call at a time. Each stage shows the argument's type as it reaches that rule, or why the rule changes nothing; the last stage is the T the compiler printed.
The T that deduction finds for the seven calls of deduce.cpp, step 7 of 7. Showing The argument's type (const char[5] ("pear")), References drop away (no reference), Top-level const goes (matched by const T&), Arrays decay (by reference: no decay), T (T = char[5]). In focus: The argument's type, Top-level const goes, T.The argument's typeconst char[5] ("pear")References drop awayno referenceTop-level const goesmatched by const T&Arrays decayby reference: no decayTT = char[5]

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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*.
  5. 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.
  6. 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.
  7. 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

  1. Question 1 of 2 const double rate = 0.18; is passed to template <typename T> void take_copy(T). What is T?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: double

    For a parameter taken by value, deduction drops references and the top-level const of the argument, because the function receives its own copy. limit, a const int, gave T = int in the lesson's program.

  2. Question 2 of 2 char code[3] = {'A', 'B', 'C'}; is passed to template <typename T> void take_ref(const T&). What is T?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: char[3]

    With const T&, the array keeps its type, so T is char[3] and the parameter is a reference to a const array of 3 chars. take_ref(marks) printed T = int[4] in the same way.

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:

Two string literals, by value and by reference

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;
}
Different answers from the two compilers, then a refusal Python · literals_session.py
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

  1. Question 1 of 2 What is T in larger("pear", "apple"), when larger takes T a, T b by value?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: const char*

    A string literal is an array of const char. Passed by value it decays to const char*, so both arguments propose the same T and the call compiles, comparing the two pointers.

  2. Question 2 of 2 Which call compares the words, so that both compilers return "pear"?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: larger(std::string("pear"), std::string("apple"))

    To compare text, the template must receive a type whose > reads the characters: std::string, as "pear"s gave, or std::string_view. Pointers to char compare as addresses.

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:

One T, two different argument types Python · conflict_session.py
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:

Interactive · step by step Why larger(1, 2.5) has no T, and two ways to give it one Step through deduction for the call. Each argument is paired with its parameter on its own; watch what each pair proposes for T.
Why larger(1, 2.5) has no T, and two ways to give it one, step 6 of 6. Showing 1, 2.5, T a (T is double: 1 becomes 1.0), T b (T is double), One T for both? (T = double, named), larger_of_two(A a, B b) (-1 and 1u: 4294967295). In focus: larger_of_two(A a, B b).1an int2.5a doubleT aT is double: 1 becomes 1.0T bT is doubleOne T for both?T = double, namedlarger_of_two(A a, B b)-1 and 1u: 4294967295

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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').
  4. 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.
  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.
  6. 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) sets T to double before the call is matched, and the int argument then converts to double the 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 type auto is then the common type of a and b, which is where a trap waits: comparing -1 with 1u converts -1 to 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.

explicit.cpp C++ · explicit.cpp
// 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));
}
Explicit arguments, a return-only parameter and two type parameters Python · explicit_session.py
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

  1. Question 1 of 2 Which calls compile with template <typename T> T larger(T a, T b);?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • larger<double>(1, 2.5)
    • larger(3, 7)

    Deduction needs every argument to propose the same T and never converts an argument to make them agree. Naming T, as in larger<double>(1, 2.5), turns deduction off for T, and then the ordinary conversions apply.

  2. Question 2 of 2 Which calls of template <typename To, typename From> To convert(From value) compile?

    Choose every answer that is right.

    A hint

    Which parameter can an argument tell the compiler about?

    Show the answer: Question 2 of 2

    Answer:

    • convert<double>(3.99)
    • convert<int>(3.99)

    Only the arguments of a call are used to deduce template arguments. A parameter that appears only in the return type must be named, which is why To is listed first: convert<int>(3.99) names To and lets the compiler deduce From.

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:

abbreviated.cpp C++ · abbreviated.cpp
// 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';
}
show() with an int, a std::string and a Fraction Python · abbreviated_session.py
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:

Interactive · answer and compare A template line, auto parameters or a generic lambda? Answer the questions about the function you are writing. The table rules out the forms that cannot do what you need, with the reason, and marks the ones that fit with a caveat.
Must the body name the type, for example to make a std::vector of it?
Must two parameters always have the same type?
Will you pass it to another function, such as an algorithm?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Three ways to write a generic function against naming the type, tying parameters together and passing it on
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

  1. Question 1 of 2 Which declaration means the same as void show(const auto& value);?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: template <typename T> void show(const T& value);

    Each auto in a parameter type invents one template parameter, so the function is an ordinary function template and can even be called with an explicit argument: show<double>(7).

  2. Question 2 of 2 auto add(auto a, auto b) is called as add(std::string("C++"), "23"). What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It compiles: a is a std::string, b a const char*, and the result is the text C++23

    add has two invented template parameters, one per auto. Each argument deduces its own, and std::string + const char* is an ordinary std::string concatenation.

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:

standard_max.cpp C++ · standard_max.cpp
// 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);
}
std::max, and a reference that dangles Python · standard_max_session.py
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:

Interactive · step by step Where the reference that std::max returns points Step through const int& top = std::max(count, 5); then the safe line. Watch what top refers to when the statement ends.
Where the reference that std::max returns points, step 5 of 5. Showing count, a temporary int (destroyed at the ;), std::max(count, 5) (returns a reference to 5), const int& top (dangling: never read it), auto best (its own int: 5). In focus: auto best.readsreturns thisrefers tocountan int: 3a temporary intdestroyed at the ;std::max(count, 5)returns a reference to 5const int& topdangling: never read itauto bestits own int: 5

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. 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

  1. Question 1 of 2 Which line keeps the larger of count and 5 safely?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: auto best = std::max(count, 5);

    std::max returns a reference to one of its arguments. When the argument is the temporary 5, a reference to the result dangles once the statement ends; both compilers warn about it. Copying the result with auto is safe.

  2. Question 2 of 2 Why does std::max(1, 2.5) not compile?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: std::max has one T for both parameters, and 1 and 2.5 propose int and double

    std::max(const T& a, const T& b) deduces T from both arguments. int and double conflict, exactly as in larger(1, 2.5), and std::max<double>(1, 2.5) settles it.

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.

The starter header and its sample tests

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;
}
The sample tests against the unfinished starter Python · generic_session.py
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.

  1. Question 1 of 16 What does larger.cpp print?

    What does this program print? Choose one answer.

    // 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);
    }
    Show the answer: Question 1 of 16

    Answer: it prints

    7
    2.5
    q
    6.5
    1.10

    larger returns a > b ? a : b for each call's own T. Characters compare by their codes, and the defaulted operator<=> of Version compares major, then minor, so the program prints 7, 2.5, q, 6.5 and 1.10.

  2. Question 2 of 16 Which template line declares the same template as template <typename T>?

    Choose one answer.

    Show the answer: Question 2 of 16

    Answer: template <class T>

    typename and class are two spellings of the same thing in a template parameter list. This track writes typename, because the parameter may be any type, not only a class.

  3. Question 3 of 16 larger.cpp calls larger with two ints, two doubles, two chars, larger<double>(4, 6.5) and two Versions. How many functions named larger does its object file define?

    Type a number.

    Show the answer: Question 3 of 16

    Answer: 4 functions

    One function per distinct T: int, double, char and Version. The call larger<double>(4, 6.5) uses the larger<double> that the two doubles needed already; nm lists the four in the lesson's session.

  4. Question 4 of 16 What did nm -C find in larger.o?

    Choose one answer.

    Show the answer: Question 4 of 16

    Answer: One function per type: larger<Version>, larger<char>, larger<double> and larger<int>

    nm -C listed four functions on both compilers, one for each T that a call needed. That is what instantiation leaves behind: ordinary functions whose names carry the template arguments.

  5. Question 5 of 16 You call a library template that compares its elements with <, passing a type that has only ==. Where does the first error point?

    Choose one answer.

    Show the answer: Question 5 of 16

    Answer: Inside the library template, at the line that uses <, with notes that lead back to your call

    An unconstrained template's requirements live in its body, so the error is found there, and the chain of instantiation notes leads back to the last frame in your own code, which is the line to change.

  6. Question 6 of 16 In GCC's error for no_greater.cpp, what does the note required from here mark?

    Choose one answer.

    Show the answer: Question 6 of 16

    Answer: Line 15, the call that made the compiler instantiate larger<Point>

    GCC writes In instantiation of 'T larger(T, T) [with T = Point]', then required from here at line 15, then the error at line 4. Clang's note "requested here" points at the same line 15.

  7. Question 7 of 16 template <typename T> void take_copy(T); is called as take_copy(marks), where marks is int marks[4]. What is T?

    Choose one answer.

    Show the answer: Question 7 of 16

    Answer: int*

    A parameter taken by value makes the array decay to a pointer to its first element before deduction, so T is int*. With a const T& parameter the array keeps its type and T would be int[4].

  8. Question 8 of 16 A function template must know the length of a plain array argument from its type. Which parameter keeps it?

    Choose one answer.

    Show the answer: Question 8 of 16

    Answer: const T&, so that T is deduced as int[4] for marks

    Only a reference parameter stops the array-to-pointer conversion, so const T& (or T&) deduces T as the array type itself, int[4], with the length in it.

  9. Question 9 of 16 Why does larger("pear", "apple") print apple with Apple clang and pear with GCC?

    Choose one answer.

    Show the answer: Question 9 of 16

    Answer: T is const char*, so the function compares the addresses of the two literals, and each compiler places them differently

    A string literal decays to const char* when it is passed by value, and > on two pointers compares where they are stored, which the standard leaves unordered for unrelated objects. Pass std::string (for example "pear"s) to compare the text.

  10. Question 10 of 16 In literals.cpp, what does the s in "pear"s do?

    Choose one answer.

    Show the answer: Question 10 of 16

    Answer: It makes the literal a std::string, through the operator in std::string_literals

    "pear"s calls a literal operator from std::string_literals and gives a std::string, so larger is instantiated for std::string and > compares the characters.

  11. Question 11 of 16 Which call fixes larger(1, 2.5) by naming the template argument?

    Choose one answer.

    Show the answer: Question 11 of 16

    Answer: larger<double>(1, 2.5)

    Explicit template arguments follow the template's name in angle brackets: larger<double>(1, 2.5). T is then known before the arguments are matched, so nothing is left to deduce.

  12. Question 12 of 16 What does larger_of_two(-1, 1u) return in explicit.cpp?

    Type a number.

    A hint

    What is the common type of int and unsigned int?

    Show the answer: Question 12 of 16

    Answer: 4294967295

    The common type of int and unsigned int is unsigned int, so -1 is converted to 4,294,967,295 before > compares it with 1, and that value is returned. Both compilers warned about the comparison from inside the instantiation.

  13. Question 13 of 16 abbreviated.cpp calls show<double>(7), where show is declared void show(const auto& value). What happens?

    Choose one answer.

    Show the answer: Question 13 of 16

    Answer: It prints 7 from the instantiation for double: the explicit argument names the invented parameter

    void show(const auto& value) is template <typename T> void show(const T& value) written short, so show<double>(7) makes T double and binds the reference to a temporary 7.0, which prints as 7.

  14. Question 14 of 16 Two parameters must have one type, and a call with an int and a double must not compile. Which declaration does that?

    Choose one answer.

    Show the answer: Question 14 of 16

    Answer: template <typename T> void f(T a, T b)

    Only a named template parameter can appear in two parameter types and make them agree. That is the case where the generic-forms table ruled auto parameters out.

  15. Question 15 of 16 Which call finds the largest of three values in one call?

    Choose one answer.

    Show the answer: Question 15 of 16

    Answer: std::max({4, 9, 2})

    std::max takes two values, two values and a comparison, or a braced list. For three values, write them in braces or put them in a range and use std::ranges::max.

  16. Question 16 of 16 Is name safe to read after const std::string& name = std::max(first, std::string("Zoya"));?

    Choose one answer.

    Show the answer: Question 16 of 16

    Answer: Not if the temporary "Zoya" is the larger: name then refers to it after it is destroyed at the semicolon

    When "Zoya" is the larger string, std::max returns a reference to the temporary, which is destroyed at the end of the statement, and name dangles. Keep the result by value, auto name = std::max(…), to be safe either way.

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

  1. Question 1 of 2 The interviewer writes larger<int>(1, 2.5). What does it return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 2, because T is int, so 2.5 becomes 2 before the comparison

    An explicit template argument fixes the parameter types, after which every argument converts as in an ordinary call. Naming the wrong type, int here, silently loses data, which is why the lesson names double.

  2. Question 2 of 2 A colleague "fixes" larger(1, 2.5) by declaring template <typename A, typename B> auto larger(A a, B b). Which call now gives a wrong answer without an error?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: larger(-1, 1u), which returns 4294967295

    Two type parameters remove the conflict but hand the comparison to the usual arithmetic conversions. Mixing signed and unsigned integers then compares -1 as the largest unsigned int; both compilers warn with -Wall -Wextra.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • template <typename T> makes a function template; each distinct T a call needs gets its own compiled function, which nm -C lists as larger<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 const and turn arrays and string literals into pointers; const T& parameters keep arrays as arrays.
  • Two string literals passed by value compare as addresses, with answers that differ between compilers; pass std::string or std::string_view to 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); prefer std::max, std::clamp and std::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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.