EduC++ Function Templates in C++

Function Templates in C++

WHY THEY EXIST: Templates let you write a function once and have the compiler generate a type-specific version for every type you call it with. Without templates, you would need to duplicate code for each type (or resort to unsafe void*).

Prerequisites: Overloading, type deduction, header/source split
Standard: Templates exist since C++98. Significantly improved in C++11 (variadic), C++14 (auto return), C++17 (if constexpr, fold expressions), and C++20 (concepts, abbreviated syntax).

1. Basic function template

T is deduced from the arguments.

   (or the same translation unit) -- the compiler
   needs the full definition at each instantiation
   site.  Putting the body in a .cpp file causes
   linker errors.

Watch out: template code must be in headers

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

// HOW TEMPLATE INSTANTIATION WORKS:
//   When you call max_of(3, 7), the compiler deduces T=int and generates a
//   concrete function — conceptually max_of_int_int — with every occurrence
//   of T replaced by int.  This is called *instantiation*.
//
//   Each unique set of template arguments creates a separate instantiation
//   with its own machine code.  max_of<int> and max_of<double> are two
//   completely independent functions in the final binary.
//
//   Instantiation happens entirely at compile time — there is zero overhead
//   compared to hand-written per-type functions.  You get type-safety and
//   code reuse for free.
//
//   Because the compiler must see the full template body to stamp out each
//   specialization, the definition must be visible at the point of use.
//   In practice this means keeping template definitions in header files.
//   Putting the body in a .cpp file and including only a declaration causes
//   linker errors (the other translation unit cannot instantiate what it
//   cannot see).
//
//   There are two kinds of instantiation:
//     - Implicit instantiation: triggered automatically when you use the
//       template (e.g., calling max_of(3, 7)).
//     - Explicit instantiation: you force the compiler to generate a
//       specific version with a declaration like:
//         template int max_of(int, int);
//       in a .cpp file.  This can reduce compile times in large projects by
//       instantiating once and linking everywhere.

2. Multiple template parameters

silently if types don't match exactly.  Use
   explicit template arguments when in doubt
   (e.g., max_of<double>(1, 2.5)).

Watch out: template argument deduction can fail

template<typename T, typename U>
auto add(T a, U b) {
    return a + b;  // Return type deduced (C++14)
}

3. Non-type template parameters

Values (not types) as template arguments.
template<int N>
constexpr int power_of_two() {
    return 1 << N;
}

template<typename T, std::size_t N>
void print_array(const T (&arr)[N]) {
    std::cout << "Array[" << N << "]: ";
    for (std::size_t i = 0; i < N; ++i) {
        std::cout << arr[i] << ' ';
    }
    std::cout << '\n';
}

4. Template specialization

Provide a custom implementation for a specific type.
template<typename T>
std::string type_name() {
    return "unknown";
}

// Full specializations for common types
template<> std::string type_name<int>()         { return "int"; }
template<> std::string type_name<double>()      { return "double"; }
template<> std::string type_name<std::string>() { return "std::string"; }

// HOW SPECIALIZATION RESOLUTION WORKS:
//   When you write type_name<int>(), the compiler must decide which
//   definition to call.  It follows a strict priority order:
//
//   1. First, look for a *full (explicit) specialization* that matches the
//      template arguments exactly.  type_name<int>() has one above, so it
//      wins immediately — returning "int" instead of "unknown".
//
//   2. If no full specialization matches, look for a *partial
//      specialization*.  This applies to class templates only — function
//      templates cannot be partially specialized.  (You can achieve a
//      similar effect for functions by using overloading or if constexpr.)
//
//   3. If no specialization matches at all, use the *primary template*.
//      For type_name<float>() — which has no specialization — the primary
//      template would be selected, returning "unknown".
//
//   This layered resolution is why you can customize type_name<int>()
//   without affecting the general template: the specialization is a
//   completely separate function that the compiler prefers when the
//   arguments match exactly.

5. SFINAE and enable_if (pre-C++20 way to constrain)

Only enable this function for integral types.

Watch out: only errors in the *immediate context* count as SFINAE errors. The immediate context is the function signature: return type, parameter types, template parameter default arguments, and explicit template arguments. If the substitution succeeds in the signature but an error occurs inside the *function body*, that is a hard compile error — not SFINAE.

template<typename T>
auto is_even(T value) -> std::enable_if_t<std::is_integral_v<T>, bool> {
    return value % 2 == 0;
}

// HOW SFINAE WORKS:
//   SFINAE = Substitution Failure Is Not An Error.
//
//   When the compiler encounters a call like is_even(42), it tries every
//   candidate template.  For this overload it substitutes T=int into the
//   signature, including the return type:
//     std::enable_if_t<std::is_integral_v<int>, bool>
//   Since is_integral_v<int> is true, enable_if_t<true, bool> yields bool,
//   the substitution succeeds, and this overload is viable.
//
//   If you called is_even(3.14), the compiler substitutes T=double:
//     std::enable_if_t<std::is_integral_v<double>, bool>
//   is_integral_v<double> is false, so enable_if_t<false, bool> has no
//   member type — the substitution *fails*.  Instead of emitting a compile
//   error, the compiler silently removes this overload from the candidate
//   set.  That is SFINAE in action.
//
//
//   C++20 concepts replaced most SFINAE usage with clearer, more readable
//   syntax.  For example, the function above can be rewritten as:
//     template<std::integral T>
//     bool is_even(T value) { return value % 2 == 0; }
//   Concepts produce better error messages and are easier to compose.

6. if constexpr: compile-time branching in templates

Different code paths for different types, no runtime cost.
template<typename T>
std::string describe(T value) {
    if constexpr (std::is_integral_v<T>) {
        return std::format("{} (integer, {} bytes)", value, sizeof(T));
    } else if constexpr (std::is_floating_point_v<T>) {
        return std::format("{:.4f} (floating, {} bytes)", value, sizeof(T));
    } else {
        return "unsupported type";
    }
}

7. Variadic templates with fold expressions (C++17)

Accept any number of arguments of any types.
template<typename... Args>
auto sum(Args... args) {
    return (args + ...);  // Unary right fold
}

template<typename... Args>
void print_all(Args&&... args) {
    ((std::cout << args << ' '), ...);  // Comma fold
    std::cout << '\n';
}

// HOW FOLD EXPRESSIONS EXPAND:
//   A fold expression collapses a parameter pack with a binary operator.
//   The compiler expands it at compile time — there is no loop at runtime.
//
//   Given args = {1, 2, 3}:
//
//   Unary right fold:  (args + ...)
//     expands to:  (1 + (2 + 3))
//     — association starts from the right.
//
//   Unary left fold:   (... + args)
//     expands to:  ((1 + 2) + 3)
//     — association starts from the left.
//
//   Binary left fold:  (init + ... + args)
//     expands to:  (((init + 1) + 2) + 3)
//     — an initial value is folded in from the left.
//
//   Binary right fold: (args + ... + init)
//     expands to:  (1 + (2 + (3 + init)))
//     — an initial value is folded in from the right.
//
//   The comma fold used in print_all above — ((std::cout << args << ' '), ...)
//   — expands to a sequence of comma-separated expressions:
//     (std::cout << a1 << ' '), (std::cout << a2 << ' '), ...
//   Each expression is evaluated left-to-right (guaranteed by the comma
//   operator), so the arguments print in order.

int main() {
    // Basic template
    std::cout << std::format("max(3, 7) = {}\n", max_of(3, 7));
    std::cout << std::format("max(3.14, 2.72) = {}\n", max_of(3.14, 2.72));
    std::cout << std::format("max(\"abc\", \"xyz\") = {}\n",
                              max_of<std::string>("abc", "xyz"));

    // Multiple template parameters
    std::cout << std::format("add(1, 2.5) = {}\n", add(1, 2.5));

    // Non-type parameters
    std::cout << std::format("2^10 = {}\n", power_of_two<10>());

    int arr[] = {10, 20, 30, 40};
    print_array(arr);  // N deduced as 4

    // Specialization
    std::cout << std::format("type_name<int>: {}\n", type_name<int>());
    std::cout << std::format("type_name<double>: {}\n", type_name<double>());

    // SFINAE
    std::cout << std::format("is_even(42) = {}\n", is_even(42));
    // is_even(3.14);  // Compile error: not integral

    // if constexpr
    std::cout << describe(42) << '\n';
    std::cout << describe(3.14159) << '\n';

    // Variadic templates
    std::cout << std::format("sum(1,2,3,4,5) = {}\n", sum(1, 2, 3, 4, 5));
    print_all("hello", 42, 3.14, 'x');

    return 0;
}