What Are Function Templates with Multiple Template Types?

When a function template has a single type parameter, all parameters using that type must receive arguments of the same type. By adding multiple template type parameters, you can write templates that accept arguments of different types, giving your generic functions much more flexibility.

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