CodeGym /Courses /C++ SELF /Type deduction and cases where it fails

Type deduction and cases where it fails

C++ SELF
Level 55, Lesson 3
Available

1. Type deduction: what it is and why it matters

If you look at templates from a beginner’s perspective, a natural thought appears: “I didn’t learn template just so I have to write <int> everywhere by hand.” And you're right: in most cases the compiler really can deduce the type T from the call arguments.

But there are cases where the compiler honestly throws up its hands: “I don’t understand which T you want.” And here the typical pain begins: the template looks simple, but the error message reads like a novel.

Technically this is called template argument deduction. In working-group reports on the standard you regularly see topics about “class template deduction” and “deduction failure” — the issue is so important that it has been polished for years even at the standard level.

Let’s build a clear model so you can:

  • predict in advance whether T will be deduced;
  • quickly fix “not deduced” by supplying an explicit <T>;
  • understand why T& is not just a “reference” but a strict contract.

2. Basic model: where the compiler gets T from

Imagine the compiler is a detective who was given a template and a call. Its job is to find such a T that the function parameters “match” the arguments. It does not try to “guess intent”; it matches type shapes. If the match is unambiguous — great. If different deductions are possible — a conflict occurs. If there is nothing to deduce from — the compiler will ask you to specify the type explicitly.

Let’s start with the friendliest case: one parameter — one argument, everything is obvious.

#include <iostream>

template <typename T>
T twice(T x) {
    return x + x;
}

int main() {
    std::cout << twice(21) << '\n';     // 42
    std::cout << twice(1.5) << '\n';    // 3
}

Here the compiler sees: twice(21) — so T = int, and twice(1.5) — so T = double.

Important: deduction happens from the call arguments, not from where you later assign the result. This is a common trap: “I assign to a double, so T must be double.” Not necessarily.

3. One T but arguments of different types

Now we approach the most common “not deduced” case. The problem looks like this: one T is used in two parameters, but you passed different types. The compiler tries to solve the equation:

  • from the first argument it gets T = int
  • from the second argument it gets T = double
  • and T must be one

Spoiler: the compiler is not a diplomat.

#include <iostream>

template <typename T>
T add_same(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add_same(1, 2) << '\n';      // 3

    // std::cout << add_same(1, 2.5) << '\n';
    // error: could not deduce T (one argument int, the other double)
}

This is not “compiler stupidity” but protection against ambiguity. Because if it simply “chose double”, questions arise: why not long double? Why not “the bigger type”? Why not “the type of the first argument”? In C++ such things cannot be left to guesswork.

How to fix it? There are two honest options: either cast the arguments to a common type yourself, or explicitly specify which T you want.

#include <iostream>

template <typename T>
T add_same(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add_same<double>(1, 2.5) << '\n'; // 3.5
}

Here we explicitly say: “calculate in double”. Then the compiler is allowed to convert 1 to double, because T is fixed.

4. When there is nowhere to deduce T from

A very “annoying” case: you wrote a template function but its parameters do not contain T. Then the compiler looks at the call and thinks: “there are no arguments containing T, nothing to match.” That is, type deduction is impossible by definition.

The most pedagogical example:

#include <iostream>

template <typename T>
T make_zero() {
    return T{};
}

int main() {
    std::cout << make_zero<int>() << '\n';      // 0
    std::cout << make_zero<double>() << '\n';   // 0
}

If you try to write make_zero() without <int>, the compiler cannot guess whether you want int, double, std::string or maybe std::vector<int> (which itself can be default-initialized with {})?

There is a similar trap: when T appears only in the return type, but not in the parameters.

template <typename T>
T read_value(); // suppose we want to read from cin

int main() {
    // auto x = read_value(); // error: T is not deduced (call without hints)
}

Remember the practical rule: a template parameter type is deduced from the call arguments. No arguments — no deduction. Therefore in such functions <T> is usually required.

5. References as a contract: why T& rejects temporaries

Up to now it was all about “which T to choose”. Now another category of problems appears: T could be deduced, but the argument cannot bind to the parameter because you chose the parameter form T&.

It is important not to panic about “references are hard”, but to see the simple idea. T& means: “I take a mutable object, I will modify it.” A temporary value (for example, 1 or a + b) is not “an object you can change”; it is a computed result that lives very briefly.

#include <iostream>

template <typename T>
void increment(T& x) {
    ++x;
}

int main() {
    int a = 10;
    increment(a);
    std::cout << a << '\n'; // 11

    // increment(1);
    // error: cannot bind temporary 1 to T&
}

Here many people perform a “ritual dance”: they start changing things randomly until it compiles. But better think this way: if increment mutates its argument, the argument must be a variable.

If you do not want to modify the argument, almost always prefer the parameter const T&. It accepts both variables and temporaries.

#include <iostream>

template <typename T>
void print_twice(const T& value) {
    std::cout << value << " | " << value << '\n';
}

int main() {
    print_twice(42);   // 42 | 42
}

This seems like a small detail, but in practice the choice between T& and const T& resolves about half of template-related “doesn’t compile” cases.

6. Tricky arguments and a quick deduction diagnostic

Now follows a block where errors especially like to spring out of the bushes. Good news: in these cases it’s not your fault, but that some C++ arguments behave in special ways.

List initialization {...}

Braces {1, 2, 3} are not always “an object of type vector”. Sometimes they are just an “initializer list” that has no single obvious type until you say where to put it.

#include <iostream>

template <typename T>
void print_one(const T& x) {
    std::cout << x << '\n';
}

int main() {
    // print_one({1, 2, 3});
    // error: the compiler does not know what T is for {1,2,3}
}

Conclusion: {...} often requires an explicit context (for example, std::vector<int>{1,2,3}), otherwise T has nothing to latch onto.

nullptr and pointer templates

You already know that nullptr can be assigned to any pointer. But template argument deduction is not “assignment”; the rules are stricter there.

#include <iostream>

template <typename T>
void reset_ptr(T* p) {
    p = nullptr; // local copy of pointer, external pointer does not change
}

int main() {
    int* p = nullptr;

    reset_ptr(p);        // T = int, OK

    // reset_ptr(nullptr);
    // error: T cannot be deduced from nullptr (it's std::nullptr_t, not T*)
}

To call with nullptr you either need to provide a pointer variable or explicitly specify the type:

reset_ptr<int>(nullptr); // now T is specified, nullptr can convert to int*

String literals "hello": array or pointer

This is where C++ shows its “retro heritage”. A string literal "hi" is not a std::string, and not even necessarily a const char* as an “original type”. Formally it's an array of characters. Then array-to-pointer decay rules apply, which you may have seen in array topics.

Because of that, the same argument can deduce T differently depending on the parameter form.

#include <iostream>

template <typename T>
void show_type_by_value(T x) {
    (void)x;
    std::cout << "by value\n"; // by value
}

template <typename T>
void show_type_by_ref(T& x) {
    (void)x;
    std::cout << "by ref\n";   // by ref
}

int main() {
    show_type_by_value("hi"); // T will be deduced so that x becomes a pointer (decay)
    // show_type_by_ref("hi"); // error: literal cannot bind to non-const T&
}

If you use const T&, the reference can bind to the literal and T will be closer to the “true form” of the argument (up to the array). You don't need to memorize all details, but it's useful to know: literals and arrays are arguments “with side effects”.

Explicitly specifying T: when that’s fine

Many beginners see func<int>(...) as a “defeat”: if the compiler didn’t deduce it, the template is bad. In practice this is a normal design tool. Sometimes the type cannot be deduced honestly, sometimes you want to control how the arguments will be converted.

For example, a “read a value from console” function logically should ask: “which type are we reading?”. It is perfectly fine to express that via <T>.

#include <iostream>
#include <string_view>

template <typename T>
T read_from_stdin(std::string_view prompt) {
    std::cout << prompt;
    T value{};
    std::cin >> value;
    return value;
}

int main() {
    int age = read_from_stdin<int>("Age? ");           // input: 20
    double price = read_from_stdin<double>("Price? "); // input: 19.99

    std::cout << age << '\n';    // 20
    std::cout << price << '\n';  // 19.99
}

Note the important point: the function has a parameter prompt, but you cannot deduce T from it (there is no T there). So <int> and <double> here are not a “hack” but part of the interface: you choose the result type.

Quick checklist: how to spot “not deduced”

When you see a compilation error like “no matching function” or “couldn't deduce template parameter”, it's useful not to panic but to run through a short mental checklist. It almost reads like a flowchart: “where does T come from, and why does it have nowhere to come from”.

flowchart TD
    A[Function template call] --> B{Are there arguments containing T?}
    B -- No --> C["Cannot deduce T: need func<T>(...) or change signature"]
    B -- Yes --> D{Do arguments imply the same T?}
    D -- No --> E[Conflict: different types -> explicit <T> or cast arguments]
    D -- Yes --> F{Does parameter T& require a mutable object?}
    F -- Yes, but a temporary was passed --> G[Make it const T& or pass a variable]
    F -- OK --> H[Deduction succeeded; any remaining issues are inside template body]

7. Example: template utilities in the “Expense Tracker”

To keep templates from remaining abstract mathematics “about T”, let’s apply them in a small console app. We'll make a simple expense tracker: add purchases and print the total.

We'll build a minimal model and adding a single entry (no fancy menus — we're here for templates, not UX).

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

struct Expense {
    std::string title;
    double amount{};
};

int main() {
    std::vector<Expense> expenses;
    expenses.push_back(Expense{"Coffee", 3.50});

    std::cout << expenses[0].title << ": " << expenses[0].amount << '\n'; // Coffee: 3.5
}

Now add two template utilities that are very typical for real code.

The first — read_from_stdin<T>(): it demonstrates a case where T is not deduced, and that's fine.

#include <iostream>
#include <string_view>

template <typename T>
T read_from_stdin(std::string_view prompt) {
    std::cout << prompt;
    T value{};
    std::cin >> value;
    return value;
}

The second — clamp_value: it demonstrates a case where T cannot be deduced from mixed types, and that is also normal.

template <typename T>
T clamp_value(T x, T lo, T hi) {
    if (x < lo) return lo;
    if (hi < x) return hi;
    return x;
}

Now we use this in main in a way that shows both categories of problems and their solutions.

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

struct Expense {
    std::string title;
    double amount{};
};

template <typename T>
T read_from_stdin(std::string_view prompt) {
    std::cout << prompt;
    T value{};
    std::cin >> value;
    return value;
}

template <typename T>
T clamp_value(T x, T lo, T hi) {
    if (x < lo) return lo;
    if (hi < x) return hi;
    return x;
}

int main() {
    std::vector<Expense> expenses;

    std::string title;
    std::cout << "Title? ";
    std::cin >> title;

    double amount = read_from_stdin<double>("Amount? "); // input: 1000
    amount = clamp_value(amount, 0.0, 100000.0);

    expenses.push_back(Expense{title, amount});
    std::cout << "Added: " << expenses.back().title << '\n'; // Added: <title>
}

Note that we must write read_from_stdin<double> because otherwise T cannot be deduced: the function parameters do not contain T, only std::string_view.

Whereas clamp_value deduces T from its arguments because T appears directly in the function parameters.

If we accidentally called clamp_value(amount, 0, 100000.0), we'd get a conflict: one argument is int, the others are double. This is not a bug but the case where “one T cannot be two different types”.

8. Common mistakes

Mistake #1: expecting the compiler to “deduce T from the type of the variable on the left”.
It often seems logical: since I write double x = add_same(1, 2.5);, T should become double. But deduction works from the call arguments, not from the assignment target. Fix it by casting arguments or using explicit add_same<double>(...).

Mistake #2: using T& “out of habit”, then wondering why literals and temporaries are rejected.
T& is a contract “I will modify the object”, so you cannot pass 1, a + b or a string literal "hi" to it. If the function only reads, almost always prefer const T&: it accepts variables and temporaries and is generally more friendly.

Mistake #3: trying to call a template without arguments and no <T>, then staring at “couldn't deduce template parameter”.
If the function has no parameters, or T only appears in the return type, the compiler cannot guess T. This is not “template complexity” but the logic of deduction: there is nothing to deduce from. So write make_zero<int>(), read_from_stdin<double>(...), etc.

Mistake #4: mixing argument types where one T is used multiple times and hoping for automatic promotion.
The call add_same(1, 2.5) seems natural (“just add numbers”), but for template deduction it is contradictory: T must be one type. Solution — cast arguments to a common type or explicitly specify T so conversions are allowed.

Mistake #5: accepting nullptr in a template with T* and being surprised that T is not deduced.
nullptr can convert to any pointer type, but you cannot deduce T from it because it is not a pointer to a specific T. If you need a call with nullptr, either pass a pointer variable or write an explicit func<int>(nullptr).

1
Task
C++ SELF, level 55, lesson 3
Locked
Value duplication
Value duplication
1
Task
C++ SELF, level 55, lesson 3
Locked
Adding doubles
Adding doubles
1
Task
C++ SELF, level 55, lesson 3
Locked
Type on Demand
Type on Demand
1
Task
C++ SELF, level 55, lesson 3
Locked
Reference binding contract
Reference binding contract
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION