Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 2 of 8

Lesson 12.1 introduced the syntax. This lesson is about the part that surprises people: what exactly T becomes.

The syntax

template <typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

template <typename T> introduces one type parameter named T. Two spellings exist:

template <typename T>   // preferred
template <class T>      // identical meaning, older style

They mean exactly the same thing. class does not mean T must be a class — maximum<int> is fine. Prefer typename; it says what it means.

T is just a name. template <typename Element> is often clearer in a container.

Deduction

You almost never write the type. The compiler works it out from the arguments:

maximum(3, 7);        // T = int
maximum(2.5, 1.5);    // T = double
maximum('a', 'z');    // T = char

That is template argument deduction. It looks at the argument types and solves for T.

Explicit arguments

When deduction cannot decide, or decides wrongly, say what you mean:

maximum<double>(3, 7);    // T = double; the int is converted

With T fixed, the arguments are converted to it rather than being used to deduce it.

Mixed types do not deduce

This is the first thing that surprises people:

// WILL NOT COMPILE
#include <iostream>

template <typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    std::cout << maximum(3, 7.5) << "\n";   // int from arg 1, double from arg 2
    return 0;
}

The compiler says so directly:

error: no matching function for call to 'maximum(int, double)'
note: candidate: 'template<class T> T maximum(T, T)'
note:   template argument deduction/substitution failed:
note:   deduced conflicting types for parameter 'T' ('int' and 'double')

Read the last line. T cannot be both. Deduction does not apply the usual arithmetic conversions first — it needs an exact match, so 3 says int and 7.5 says double and there is no answer.

Three fixes, and which you pick says something different:

maximum<double>(3, 7.5);      // fix T explicitly
maximum(3.0, 7.5);            // make the call site consistent

or change the template to take two parameters, which is the next section.

Two type parameters

#include <iostream>
#include <string>
#include <vector>

// Two type parameters: the arguments no longer have to match.
template <typename A, typename B>
auto add(A a, B b) {
    return a + b;             // return type deduced from the expression
}

// Deducing from a container's element type.
template <typename T>
void print_all(const std::vector<T>& values) {
    for (const T& v : values) {
        std::cout << v << " ";
    }
    std::cout << "\n";
}

// A deduced array size, which is a non-type parameter (lesson 12.5).
template <typename T, std::size_t N>
std::size_t length_of(const T (&)[N]) {
    return N;
}

int main() {
    std::cout << add(3, 7.5) << "\n";
    std::cout << add(std::string("count: "), "five") << "\n";

    std::vector<int> numbers = {1, 2, 3};
    std::vector<std::string> words = {"alpha", "beta"};
    print_all(numbers);
    print_all(words);

    int raw[7] = {};
    std::cout << "array length " << length_of(raw) << "\n";
    return 0;
}
10.5
count: five
1 2 3 
alpha beta 
array length 7

Three things in that program are worth naming.

auto as the return type asks the compiler to deduce it from the return expression. int + double is double, so add(3, 7.5) returns double and prints 10.5.

print_all deduces from a pattern, not from a whole type. The parameter is const std::vector<T>&, so passing a std::vector<std::string> deduces T = std::string. This pattern-matching is how most library templates work.

length_of recovers an array's length, which a plain int* parameter loses (lesson 7.5). The size is part of the array's type, so a template can capture it.

The deduction rules that catch people

For a by-value parameter, deduction drops top-level const and references:

#include <iostream>
#include <type_traits>

template <typename T>
void by_value(T) {
    std::cout << "T is const: " << std::is_const_v<T>
              << ", T is reference: " << std::is_reference_v<T> << "\n";
}

template <typename T>
void by_const_ref(const T&) {
    std::cout << "T is const: " << std::is_const_v<T>
              << ", T is reference: " << std::is_reference_v<T> << "\n";
}

int main() {
    const int ci = 42;
    const int& cr = ci;

    std::cout << std::boolalpha;

    by_value(ci);        // T deduced as int — the const is dropped
    by_value(cr);        // T deduced as int — the reference is dropped too
    by_const_ref(ci);    // T deduced as int — const is in the parameter, not T
    return 0;
}
T is const: false, T is reference: false
T is const: false, T is reference: false
T is const: false, T is reference: false

All three say false, and that is the point. Passing a const int& to a by-value parameter deduces T = int, because the function gets its own copy and copies are not const.

For const T&, the const is written in the parameter, so T itself is still plain int.

Pass by value or by const reference?

The same rule as ordinary functions (lesson 6.4):

template <typename T>
T maximum(T a, T b);                       // copies both arguments

template <typename T>
const T& maximum(const T& a, const T& b);  // no copies

For int and double the copy is free. For a std::string or a std::vector it is not — by value copies the whole contents on every call.

Default to const T& for a template, because you do not know what T will be. The exception is when you need your own copy anyway, in which case take it by value and move from it (lesson 9.10).

But note the trap in the second signature: returning const T& means returning a reference to one of the arguments. If the caller passes a temporary, the reference dangles the moment the statement ends (lesson 8.5). This is a real bug in real code — std::max has exactly this hazard.

Overloading a template

A non-template function wins over a template when both match exactly:

#include <iostream>
#include <cstring>

template <typename T>
T maximum(T a, T b) {
    std::cout << "[template] ";
    return (a > b) ? a : b;
}

// A plain overload, for the case where the template does the wrong thing.
const char* maximum(const char* a, const char* b) {
    std::cout << "[overload] ";
    return (std::strcmp(a, b) > 0) ? a : b;
}

int main() {
    std::cout << maximum(3, 7) << "\n";
    std::cout << maximum("apple", "banana") << "\n";
    return 0;
}
[template] 7
[overload] banana

That overload exists because the template would be wrong here. With T = const char*, a > b compares pointer addresses, not the text — it would return whichever string happens to sit higher in memory. The overload uses strcmp, which compares the characters.

This is the general shape: write the template for the common case, and add a plain overload for the type it gets wrong.

Try it

Write a template clamp(value, low, high) returning value limited to the range. Test it with int and double. Then try calling it with mixed types and fix the error two different ways. Finally, write a version taking const T& and explain when each is preferable.

Checkpoint

  • template <typename T> and template <class T> are identical; prefer typename.
  • Deduction solves for T from the argument types, with no arithmetic conversions first.
  • maximum(3, 7.5) fails because T cannot be both int and double.
  • Fix it by making the call consistent, giving T explicitly, or using two type parameters.
  • By-value deduction drops top-level const and references.
  • Templates can deduce from patterns like const std::vector<T>& and const T (&)[N].
  • Prefer const T& parameters, since you do not know how expensive T is to copy.
  • Returning const T& can dangle when the caller passes a temporary.
  • A non-template overload beats the template on an exact match — useful when the template is wrong.

Questions about this lesson

Log in to ask a question.