Skip to content
C++
Language
since C++98
Intermediate

Function Template

A parameterized function definition from which the compiler generates concrete functions for each unique set of template arguments, enabling type-generic programming.

Function Templatesince C++98

A function template is a blueprint from which the compiler generates a distinct concrete function for each unique combination of template arguments supplied or deduced at the point of call.

Overview

A function template defers type selection to the call site. The compiler instantiates a concrete function β€” called a template specialization β€” by substituting deduced or explicitly provided arguments into the template parameters. Unlike overloads, which require a separate definition per type, a single template covers every type that satisfies the template's constraints.

This mechanism underlies virtually all generic programming in C++: every algorithm in <algorithm>, every std::swap, every std::make_unique is a function template. Understanding how deduction, substitution, and overload resolution interact is prerequisite knowledge for any serious C++ work.

Syntax

cpp
// Primary form β€” C++98
template<typename T>
T clamp(T value, T lo, T hi) {
    return value < lo ? lo : value > hi ? hi : value;
}

// Multiple type parameters with trailing return type β€” C++11
template<typename T, typename U>
auto add(T lhs, U rhs) -> decltype(lhs + rhs) {
    return lhs + rhs;
}

// Non-type template parameter
template<typename T, std::size_t N>
T sum(const T (&arr)[N]) {
    T result{};
    for (const auto& v : arr) result += v;
    return result;
}

// Abbreviated function template β€” C++20
auto square(auto x) { return x * x; }

typename and class are interchangeable when introducing type parameters; prefer typename in modern code.

Template Argument Deduction

The compiler derives template arguments from the function argument types. Deduction rules were established in C++98, substantially clarified in C++11 (especially around reference collapsing), and extended in C++17 (class template argument deduction, if constexpr).

cpp
template<typename T>
void inspect(T x);

inspect(42);        // T = int
inspect(3.14f);     // T = float
inspect("hello");   // T = const char*
inspect<long>(42);  // explicit: T = long, no deduction

Deduction never crosses implicit conversions. clamp(1, 0.0, 2.0) fails because int and double produce conflicting deductions for T. The caller must either provide an explicit argument (clamp<double>(1, 0.0, 2.0)) or the template must use multiple parameters.

Forwarding References (C++11)

A parameter declared as T&& inside a function template is a forwarding reference, not an rvalue reference. It binds to both lvalues and rvalues and collapses correctly via reference collapsing rules.

cpp
template<typename T>
void relay(T&& arg) {
    target(std::forward<T>(arg));  // C++11: preserves value category
}

When an lvalue int x is passed, T deduces to int& and T&& collapses to int&. When an rvalue is passed, T deduces to int and T&& stays int&&. This is the basis of every forwarding factory in the standard library.

Examples

Type-Generic Algorithm

cpp
template<typename RandomIt, typename Compare>
void insertion_sort(RandomIt first, RandomIt last, Compare cmp) {
    for (auto it = first; it != last; ++it) {
        auto key = std::move(*it);              // C++11
        auto pos = std::upper_bound(first, it, key, cmp);
        std::move_backward(pos, it, std::next(it));  // C++11
        *pos = std::move(key);
    }
}

// Usage
std::vector<int> v{5, 3, 8, 1};
insertion_sort(v.begin(), v.end(), std::less<>{});  // C++14: transparent comparator

Return Type Deduction

C++14 permits auto return types without a trailing expression when the return type can be deduced from the body:

cpp
// C++11: trailing return type required
template<typename C>
auto first(const C& c) -> decltype(*c.begin());

// C++14: body-deduced return type
template<typename C>
auto first(const C& c) {
    return *c.begin();
}

Variadic Templates (C++11)

Parameter packs allow a template to accept any number of arguments:

cpp
// C++11 recursive approach
template<typename T>
T sum_all(T v) { return v; }

template<typename T, typename... Rest>
T sum_all(T first, Rest... rest) {
    return first + sum_all(rest...);
}

// C++17: fold expression β€” preferred
template<typename... Ts>
auto sum_all(Ts... vs) {
    return (vs + ...);
}

sum_all(1, 2.5, 3L);  // 6.5

SFINAE-Based Constraints (C++11)

When substitution into a template signature produces an ill-formed type, the compiler silently discards that overload rather than raising an error. This lets you gate overloads on type properties:

cpp
#include <type_traits>

template<typename T,
         std::enable_if_t<std::is_integral_v<T>, int> = 0>  // C++14: _v/_t helpers
T next_pow2(T n) {
    T r = 1;
    while (r < n) r <<= 1;
    return r;
}

next_pow2(7);     // OK: int
next_pow2(3.14);  // ill-formed: substitution failure, no viable overload

Concepts (C++20)

C++20 concepts replace SFINAE constraints with direct, readable syntax that generates actionable error messages:

cpp
#include <concepts>

template<std::integral T>      // C++20: concept as constraint
T next_pow2(T n) {
    T r = 1;
    while (r < n) r <<= 1;
    return r;
}

// Equivalent with requires clause
template<typename T>
    requires std::integral<T>
T next_pow2(T n) { /* ... */ }

// Abbreviated: auto + concept β€” C++20
void process(std::integral auto n);

Best Practices

Constrain at the interface, not inside the body. An unconstrained template accepts any type and produces errors deep in instantiation, far from the call site. In C++20, use concepts. In C++17 and earlier, prefer static_assert with a clear message or std::enable_if_t at the signature boundary.

Use if constexpr (C++17) to branch on type properties within a single template rather than creating sibling overloads whose interaction with deduction is subtle:

cpp
template<typename T>
void serialize(const T& v) {
    if constexpr (std::is_trivially_copyable_v<T>) {  // C++17
        std::memcpy(buf, &v, sizeof(T));
    } else {
        v.serialize(buf);
    }
}

Prefer noexcept with conditionality when the template may or may not throw depending on its argument types:

cpp
template<typename T>
void my_swap(T& a, T& b)
    noexcept(std::is_nothrow_move_constructible_v<T> &&
             std::is_nothrow_move_assignable_v<T>)  // C++11
{
    T tmp = std::move(a);
    a = std::move(b);
    b = std::move(tmp);
}

Mark [[nodiscard]] (C++17) on templates returning computed values β€” the same discipline applies as for regular functions.

Common Pitfalls

Two-phase lookup and dependent typename. Names that depend on a template parameter are looked up at instantiation time (phase 2), but the parser must be told whether a dependent qualified name is a type:

cpp
template<typename C>
void print_all(const C& c) {
    typename C::const_iterator it = c.begin();  // typename required
    for (; it != c.end(); ++it)
        std::cout << *it << '\n';
}

Omitting typename causes a parse error because the compiler tentatively treats C::const_iterator as a value during phase 1.

Function templates cannot be partially specialized. Only full specialization is permitted. When specialization on a subset of parameters is needed, delegate to a class template:

cpp
// Ill-formed: partial specialization of function template
// template<typename T> void f<T, int>(T, int) {}  // error

// Correct: overload
template<typename T>
void f(T, int) { /* int-specific path */ }

// Or: tag-dispatch to a class template that can be partially specialized
template<typename T, typename U>
struct f_impl { static void call(T, U) { /* general */ } };

template<typename T>
struct f_impl<T, int> { static void call(T, int) { /* int path */ } };

template<typename T, typename U>
void f(T a, U b) { f_impl<T, U>::call(a, b); }

Explicit instantiation to reduce compile time. In large projects, placing extern template declarations (C++11) in headers suppresses redundant instantiation across translation units. The explicit instantiation definition must appear in exactly one .cpp file:

cpp
// header
extern template void serialize<MyType>(const MyType&);  // C++11

// one .cpp
template void serialize<MyType>(const MyType&);

Deduction failures from array-to-pointer decay. A parameter declared as T arr[] decays the argument to a pointer before deduction, losing the array bound. Use T (&arr)[N] to retain it.

Abbreviated Function Templates (C++20)

C++20 lets you write function templates without a template<> head by using auto as a parameter type. Every auto parameter introduces a fresh, independent type parameter β€” the same way auto parameters work in generic lambdas (C++14).

cpp
// C++17: explicit template head required
template<typename T>
void print(const T& value);

// C++20: abbreviated β€” equivalent to the above
void print(const auto& value);

Each auto in the parameter list is a distinct type parameter:

cpp
// T and U are independent β€” equivalent to template<typename T, typename U>
auto add(auto lhs, auto rhs) { return lhs + rhs; }

// This is NOT the same as template<typename T> T add(T, T)
// add(1, 2.0) is well-formed: int + double

To tie two parameters to the same type, you still need an explicit template<typename T> head.

Constrained Auto

Prefix auto with a concept name to constrain the deduced type β€” this is called constrained auto:

cpp
#include <concepts>

// Only accepts types satisfying std::integral
void increment(std::integral auto& n) { ++n; }

// Equivalent explicit form
template<std::integral T>
void increment(T& n) { ++n; }

The Concept auto syntax was chosen over Concept alone because using a bare concept name as a type would be ambiguous: the compiler could not tell, without knowing whether animal is a type or a concept, whether void f(animal x) is a template or a regular function.

Constrained auto also works for return types and variable declarations:

cpp
std::integral auto get_count();  // return type must satisfy std::integral
std::floating_point auto ratio = compute();  // variable type constrained

Concepts and the requires Clause (C++20)

C++20 provides four syntactically distinct β€” but semantically equivalent β€” ways to apply a concept constraint to a function template. All four constrain the same T; the choice is stylistic.

cpp
#include <concepts>

// Form 1: concept as template parameter qualifier (terse, preferred for single constraints)
template<std::integral T>
T clamp(T v, T lo, T hi);

// Form 2: requires clause after the template parameter list
template<typename T>
    requires std::integral<T>
T clamp(T v, T lo, T hi);

// Form 3: trailing requires clause (after the parameter list, before the body)
template<typename T>
T clamp(T v, T lo, T hi) requires std::integral<T>;

// Form 4: abbreviated (constrained auto)
auto clamp(std::integral auto v, std::integral auto lo, std::integral auto hi);

Forms 2 and 3 accept arbitrary Boolean constant expressions, not only named concepts, which makes them useful for compound constraints:

cpp
template<typename T>
    requires std::integral<T> && (sizeof(T) >= 4)
T wide_shift(T x, int n);

// requires expression for ad-hoc structural requirements
template<typename T>
    requires requires(T a, T b) { a + b; a < b; }
T bounded_add(T a, T b);

Constraint Ordering and Subsumption

When multiple overloads exist, the compiler picks the most constrained one. A constraint P subsumes Q if P being satisfied logically implies Q is satisfied. Subsumption only works through named concepts β€” raw type-trait expressions are treated as opaque atomic constraints and never subsume each other:

cpp
template<typename T>
concept Decrementable = requires(T t) { --t; };

template<typename T>
concept BidirectionalIt = Decrementable<T> && requires(T t) { *t; };

template<Decrementable T> void advance(T& it);     // #1
template<BidirectionalIt T> void advance(T& it);   // #2 β€” more constrained, wins for iterators

Disjunctions and Negations

cpp
// Accept signed OR floating-point types
template<typename T>
    requires std::signed_integral<T> || std::floating_point<T>
T abs_val(T x) { return x < 0 ? -x : x; }

Constraints are short-circuited left-to-right: if the left operand of && is not satisfied, the right operand is never substituted, which prevents errors outside the immediate substitution context.

if constexpr for Compile-Time Branching (C++17)

if constexpr evaluates its condition at compile time and discards the non-taken branch entirely. Discarded branches are syntax-checked but never instantiated, so they can legally contain code that would be ill-formed for the current type β€” as long as the syntax is valid.

Why a Runtime if Is Not Enough

A plain if compiles all branches for every instantiation:

cpp
template<typename T>
std::string to_str(T t) {
    if (std::is_convertible_v<T, std::string>)
        return t;               // error when T = int: can't return int as string
    else
        return std::to_string(t);  // error when T = std::string: no to_string(string)
}

Both errors are real even though only one branch will execute at runtime. if constexpr fixes this:

cpp
template<typename T>
std::string to_str(T t) {
    if constexpr (std::is_convertible_v<T, std::string>)  // C++17
        return std::string(t);
    else
        return std::to_string(t);
}

The discarded branch is not instantiated, so there is no type error.

Replacing Tag Dispatch

Before C++17, selecting between code paths at compile time required tag dispatch or SFINAE overloads. if constexpr consolidates this into a single readable function:

cpp
// Pre-C++17: two overloads with enable_if
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, bool>
close_enough(T a, T b) { return std::abs(a - b) < T(1e-6); }

template<typename T>
std::enable_if_t<!std::is_floating_point_v<T>, bool>
close_enough(T a, T b) { return a == b; }

// C++17: single function, same semantics
template<typename T>
constexpr bool close_enough(T a, T b) {
    if constexpr (std::is_floating_point_v<T>)
        return std::abs(a - b) < T(1e-6);
    else
        return a == b;
}

Compile-Time Dispatch on Non-Type Template Parameters

if constexpr is also valuable with non-type parameters, where the condition is a constant known at instantiation time:

cpp
template<std::size_t N>
constexpr auto make_mask(int const (&indices)[N]) {
    uint64_t mask = 0;
    for (int i = 0; i < static_cast<int>(N); ++i)
        if (indices[i] >= 0) mask |= uint64_t(1) << i;
    if constexpr (N <= 8)  return static_cast<uint8_t>(mask);
    else if constexpr (N <= 16) return static_cast<uint16_t>(mask);
    else if constexpr (N <= 32) return static_cast<uint32_t>(mask);
    else return mask;
}

The return type itself differs between instantiations β€” something impossible with a runtime if.

Why You Should Rarely Explicitly Specialize Function Templates

Full (explicit) specialization of function templates looks tempting but has a fundamental pitfall: specializations do not participate in overload resolution. Only base templates overload. This produces non-obvious call resolution when the order of declarations matters.

The Dimov/Abrahams Example

The classic demonstration (from Herb Sutter's C/C++ Users Journal article "Why Not Specialize Function Templates"):

cpp
template<typename T> void f(T);    // (a) base template
template<typename T> void f(T*);   // (b) overload of (a)
template<> void f<>(int*);         // (c) specialization β€” of which base?

(c) specializes (b) because (b) is the best match at the point of the specialization's declaration. Call f((int*)nullptr) β†’ calls (c). So far so good.

Now move the declaration of (b) after (c):

cpp
template<typename T> void f(T);    // (a)
template<> void f<>(int*);         // (c) β€” now specializes (a), not (b)!
template<typename T> void f(T*);   // (b)

f((int*)nullptr);  // calls (b), NOT (c) β€” overload resolution ignores specializations

The result changes purely because of declaration order. The root cause: overload resolution considers only base templates. It picks the best base template first, then checks for a matching specialization of that template. When (b) is declared after (c), overload resolution picks (b) as the best base template, and (c) (a specialization of (a)) is never considered.

The Fix: Use Overloads or Class Template Delegation

cpp
// Correct: a plain overload always participates in overload resolution
void f(int*) { /* int* path */ }

// Or: delegate to a class template that can be partially specialized
template<typename T>
struct FImpl {
    static void call(T v) { /* general path */ }
};

template<typename T>
struct FImpl<T*> {
    static void call(T* p) { /* pointer path */ }
};

template<typename T>
void f(T v) { FImpl<T>::call(v); }

The practical rule: if you need type-specific behaviour for a function template, write an overload (for non-template types) or delegate to a class template (for partial specialization). Reserve full specialization of function templates for cases where you genuinely want to specialize an existing base template without altering overload resolution β€” and document why.

See Also