The single-type limitation

Consider a template that adds two values:

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

This works when both arguments share the same type:

add(10, 20);      // OK: both int, returns 30
add(1.5, 2.5);    // OK: both double, returns 4.0

But mixing types fails:

add(10, 2.5);     // Error: T cannot be both int and double

The compiler cannot deduce T when arguments suggest different types. Template argument deduction looks for exact matches without performing conversions.

Three ways to handle mixed types

Approach 1: Cast the arguments

Force both arguments to the same type:

add(static_cast<double>(10), 2.5);  // Both double now

This works but clutters the code.

Approach 2: Specify the type explicitly

Bypass deduction entirely:

add<double>(10, 2.5);  // Instantiates add<double>, converts 10 to 10.0

When you explicitly provide the template argument, normal conversion rules apply to the function arguments.

Approach 3: Multiple template parameters

The cleanest solution uses independent type parameters:

template <typename T, typename U>
??? add(T a, U b)
{
    return a + b;
}

Now T and U can resolve independently. But what should the return type be?

The return type problem

With two parameters of different types, choosing the return type is tricky:

template <typename T, typename U>
T add(T a, U b)      // Returns T
{
    return a + b;
}

If T is int and U is double:

add(5, 2.7);  // Returns int! Result is 7, not 7.7

The addition produces 7.7 (a double), but converting to int truncates it. Using U as the return type has the same problem in reverse.

Let the compiler deduce the return type

Use auto to deduce the return type from the expression:

#include <iostream>

template <typename T, typename U>
auto add(T a, U b)
{
    return a + b;
}

int main()
{
    std::cout << add(5, 2.7) << '\n';    // Prints 7.7 (double)
    std::cout << add(3.14, 2) << '\n';   // Prints 5.14 (double)
    std::cout << add(10, 20) << '\n';    // Prints 30 (int)

    return 0;
}

The compiler determines the return type from a + b using the usual arithmetic conversion rules. When mixing int and double, the result is double.

Practical examples

Calculating areas with different measurement types:

template <typename T, typename U>
auto calculateArea(T width, U height)
{
    return width * height;
}

calculateArea(5, 3.5);      // int * double = double
calculateArea(2.5, 4);      // double * int = double
calculateArea(10, 20);      // int * int = int

Interpolating between values:

template <typename T, typename U>
auto lerp(T start, U end, double t)
{
    return start + (end - start) * t;
}

lerp(0, 100, 0.5);        // Halfway between 0 and 100
lerp(0.0, 1.0, 0.25);     // Quarter way between 0.0 and 1.0

Combining different numeric types:

template <typename T, typename U>
auto average(T a, U b)
{
    return (a + b) / 2.0;
}

average(10, 20);       // int, int -> double (15.0)
average(3, 4.5);       // int, double -> double (3.75)

Abbreviated function templates (C++20)

C++20 provides a shorthand using auto for parameters:

auto add(auto a, auto b)
{
    return a + b;
}

This is equivalent to:

template <typename T, typename U>
auto add(T a, U b)
{
    return a + b;
}

Each auto parameter becomes an independent template parameter. This syntax is concise and works well for simple templates.

Limitations of abbreviated templates:

// Cannot enforce both parameters have the same type
auto process(auto x, auto y);  // x and y can differ

// For same-type constraint, use traditional syntax
template <typename T>
T process(T x, T y);  // x and y must match

Template overloading

Function templates can be overloaded like regular functions:

#include <iostream>

template <typename T>
auto multiply(T a, T b)
{
    std::cout << "same types\n";
    return a * b;
}

template <typename T, typename U>
auto multiply(T a, U b)
{
    std::cout << "different types\n";
    return a * b;
}

int main()
{
    multiply(3, 4);       // Calls single-type version
    multiply(3, 4.5);     // Calls two-type version
    multiply(2.5, 1.5);   // Calls single-type version

    return 0;
}

When multiple templates could match, the compiler prefers the more specialized one. The single-parameter template is more restrictive (requires matching types), so it's preferred when applicable.

Summary

Approach Syntax Best for
Single parameter template <typename T> Enforcing same type
Multiple parameters template <typename T, typename U> Allowing different types
Abbreviated (C++20) auto func(auto, auto) Simple multi-type templates

Key points:

  • Template deduction doesn't perform type conversions
  • Multiple template parameters allow independent type resolution
  • Use auto return type to avoid choosing between T and U
  • The compiler deduces return type from the actual expression
  • More specialized templates are preferred during overload resolution
  • C++20 abbreviated templates simplify common patterns