Templates in C++: Function and Class Templates

Templates in C++ let you write type-safe, reusable code that the compiler specializes for the types you use, often with zero runtime overhead. This beginner-friendly guide covers function and class templates with clear examples, outputs, and “Try it yourself” challenges after each section.

🔹 What are Templates in C++?

Templates in C++ enable generic programming: you write an algorithm once and the compiler generates optimized versions for each type you use. This gives you the flexibility of “generic” code with the performance of hand-written, type-specific code.

  • Function templates: generic functions parameterized by type.
  • Class templates: generic types (containers, math types, adapters).
  • Non-type parameters: constants as template parameters (sizes, flags).
  • Specialization: customize behavior for specific types.
  • Concepts (C++20): add readable constraints to templates.

Try it yourself

  • Name two functions you’ve duplicated for int and double. Could they be a single function template?
  • List one class (e.g., Box) that could become a class template with different stored types.

🔹 Function Templates: The Basics

Function templates look like regular functions, but with a template<typename T> parameter that represents a type. The compiler deduces T from arguments at the call site and generates the concrete function for you.

#include <iostream>
#include <string>
using namespace std;
// A generic add that works for any T supporting operator+
template <typename T>
T add(T a, T b) {
    return a + b; // must be valid for T
}
int main() {
    cout << add(2, 3) << "\n";           // T = int
    cout << add(2.5, 1.2) << "\n";       // T = double
    cout << add(string("Hi "), string("C++")) 
    << "\n"; // T = std::string
}

Output

5
3.7
Hi C++

The compiler instantiates add<int>, add<double>, and add<std::string> at compile time. No virtual calls, no runtime overhead.

Try it yourself

  • Create maxOf(T a, T b) that returns the larger value.
  • Write swapValues(T&, T&) and test with int, double, and std::string.

🔹 Class Templates: Generic Types

Class templates parameterize types themselves. Think of std::vector<T>: it’s one class template, instantiated for many types like vector<int>, vector<double>, etc.

#include <iostream>
#include <utility>
using namespace std;
template <typename T>
class Box {
    T value;
public:
    Box() = default;
    explicit Box(T v) : value(std::move(v)) {}
    void set(T v) { value = std::move(v); }
    const T& get() const { return value; }
};
int main() {
    Box<int> bi(42);
    Box<string> bs(string("templates"));
    cout << bi.get() << "\n";
    cout << bs.get() << "\n";
    bi.set(100);
    cout << bi.get() << "\n";
}

Output

42
templates
100

Each instantiation generates a distinct type: Box<int> is different from Box<string>, so type safety stays strong.

Try it yourself

  • Add void print() const to Box that prints the value. Test with int and std::string.
  • Create Pair<T, U> storing two different types; add first() and second() methods.

🔹 Overload Resolution and Deduction

When both template and non-template overloads match, non-template overloads have priority. Also, type deduction must succeed; otherwise you may need explicit template arguments or conversions.

#include <iostream>
using namespace std;
void show(int x) { cout << "non-template: " << x << "\n"; }
template <typename T>
void show(T x) { cout << "template: " << x << "\n"; }
int main() {
    show(5);          // prefers non-template
    show(3.14);       // template version (no non-template match)
    show<const char*>("hi"); // explicit template arg
}

Output

non-template: 5
template: 3.14
template: hi

Remember: template argument deduction must produce a unique type, or you’ll get ambiguity or compilation errors.

Try it yourself

  • Add another non-template show(double) and see which overload is picked for 3.14.
  • Call show('A') and observe which overload is chosen on your compiler.

🔹 Template Specialization (Function and Class)

Specialization lets you customize behavior for specific types while keeping a generic default. Use full specialization for a specific type. Class templates also support partial specialization (based on patterns), but function templates do not.

#include <iostream>
#include <string>
using namespace std;
// Generic print template
template <typename T>
void print(const T& x) { cout << x << "\n"; }
// Full specialization for bool
template <>
void print<bool>(const bool& x) 
{ cout << (x ? "true" : "false") << "\n"; }
// Class template + partial specialization
template <typename T>
struct Holder {
    T value;
    void dump() const 
    { cout << "Holder<T>: " 
      << value << "\n"; 
    }
};
// Partial specialization for pointer types
template <typename T>
struct Holder<T*> {
    T* value{};
    void dump() const 
    { cout << "Holder<T*>: " 
      << (value ? "ptr set" : "null") << "\n"; 
    }
};
int main() {
    print(10);     // generic
    print(true);   // specialized for bool
    Holder<int> h1{42};
    Holder<int*> h2{nullptr};
    h1.dump();
    h2.dump();
}

Output

10
true
Holder<T>: 42
Holder<T*>: null

Use specialization to tweak behavior for special cases without duplicating the whole implementation.

Try it yourself

  • Add a full specialization print<std::string> that quotes the string.
  • Create a partial specialization Holder<const T> that prints “const T”.

🔹 Non-Type Template Parameters (NTTP)

Templates can take values (like integers) as parameters. This is useful for sizes and policies known at compile time, enabling optimizations and stronger type checks.

#include <array>
#include <iostream>
using namespace std;
template <typename T, size_t N>
struct StaticBuffer {
    array<T, N> data{};
    constexpr size_t size() const 
    { return N; }
    T& operator[](size_t i) 
    { return data[i]; }
    const T& operator[](size_t i) const 
    { return data[i]; }
};
int main() {
    StaticBuffer<int, 4> buf;
    for (size_t i = 0; i < buf.size(); ++i) 
    buf[i] = static_cast<int>(i * 10);
    for (size_t i = 0; i < buf.size(); ++i) 
    cout << buf[i] << " ";
    cout << "\n";
}

Output

0 10 20 30 

N is part of the type: StaticBuffer<int, 4> and StaticBuffer<int, 8> are different types, preventing accidental mismatch in APIs.

Try it yourself

  • Create Matrix<T, R, C> with fixed rows/cols and add multiply for compatible sizes.
  • Add a boolean NTTP (e.g., bool Debug) to toggle logging at compile time.

🔹 Concepts (C++20): Constraining Templates

Concepts let you express requirements on template parameters with readable errors and cleaner overloads. They replace many SFINAE tricks with clear syntax.

#include <concepts>
#include <iostream>
using namespace std;
template <typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template <Number T>
T safe_add(T a, T b) {
    return a + b;
}
int main() {
    cout << safe_add(2, 3) 
    << "\n";   // OK (integral)
    cout << safe_add(2.5, 1.5) 
    << "\n"; // OK (floating)
    // safe_add(string("a"), string("b")); // ERROR: doesn't satisfy Number
}

Output

5
4

With concepts, you get descriptive error messages when constraints aren’t met, making Templates in C++ more approachable.

Try it yourself

  • Define a concept HasSize requiring obj.size(), then write printSize for any HasSize type.
  • Create two constrained overloads of sum for integral vs floating types and print a different label.

🔹 Variadic Templates and Fold Expressions

Variadic templates accept any number of template arguments. Fold expressions (C++17) make operations over parameter packs concise and readable.

#include <iostream>
using namespace std;
// Sum arbitrary many numbers using a fold expression
template <typename... Args>
auto sum(Args... args) {
    return (args + ...); // (((a + b) + c) + ...)
}
int main() {
    cout << sum(1, 2, 3, 4) 
    << "\n";
    cout << sum(1.5, 2.0, 3.5) 
    << "\n";
}

Output

10
7

Variadics are powerful for logging, tuple utilities, or adapter layers where argument counts vary.

Try it yourself

  • Write printAll(args...) that prints values separated by commas using a fold over ((cout << args << ", "), ...).
  • Implement allTrue(args...) with a logical AND fold.

🔹 Templates and Separate Compilation

Templates must be visible at the point of use (typically in headers). If you put definitions only in .cpp files, you’ll hit linker errors. Options: keep definitions in headers or use explicit instantiation declarations/definitions.

// math.hpp
#pragma once
template <typename T>
T square(T x) { return x * x; } // define in header so callers can instantiate
// main.cpp
#include <iostream>
#include "math.hpp"
int main() {
    std::cout << square(7) 
    << "\n";
}

Advanced: explicit instantiation lets you keep definitions in a .cpp but only for specific types you declare (trade flexibility for compile speed and smaller binaries).

Try it yourself

  • Create stats.hpp with mean(T*, size_t) in the header and include it in two different .cpp files. Build and run.
  • Research and try extern template / explicit instantiation in one TU for double.

🔹 Mini Project: Generic 2D Vector with Concepts

Combine class templates, operator overloading, and concepts to build a small math type. This showcases practical Templates in C++ with readable constraints.

#include <concepts>
#include <iostream>
#include <cmath>
using namespace std;
template <typename T>
concept Arithmetic = std::integral<T> || std::floating_point<T>;
template <Arithmetic T>
struct Vec2 {
    T x{}, y{};
    Vec2() = default;
    Vec2(T x_, T y_) : x(x_), y(y_) {}
    Vec2 operator+(const Vec2& other) const 
    { return {x + other.x, y + other.y}; }
    Vec2 operator-(const Vec2& other) const 
    { return {x - other.x, y - other.y}; }
    Vec2 operator*(T k) const 
    { return {x * k, y * k}; }
    T dot(const Vec2& other) const 
    { return x * other.x + y * other.y; }
    double magnitude() const {
        return std::sqrt(static_cast<double>(x) * x + 
        static_cast<double>(y) * y);
    }
};
template <Arithmetic T>
ostream& operator<<(ostream& os, 
    const Vec2<T>& v) {
    return os << "(" << v.x 
    << ", " << v.y << ")";
}
int main() {
    Vec2<int> a(2, 3), b(4, 1);
    Vec2<double> c(1.5, 2.5);
    cout << (a + b) << "\n";
    cout << (a - b) << "\n";
    cout << (c * 2.0) << "\n";
    cout << "dot(a,b)=" 
    << a.dot(b) << "\n";
    cout << "||c||=" 
    << c.magnitude() << "\n";
}

Output

(6, 4)
(-2, 2)
(3, 5)
dot(a,b)=11
||c||=2.91548

This template is reusable for any arithmetic type and benefits from compile-time checks and inlining for performance.

Try it yourself

  • Add normalize() returning a unit vector (guard against zero length).
  • Implement operator== with a tolerance for floating-point types.

🔹 Best Practices and Common Pitfalls

  • Keep template definitions in headers (or use explicit instantiation) to avoid link errors.
  • Use concepts (C++20) or static_assert to provide clear diagnostics and constrain templates.
  • Avoid overly generic templates that accept types they can’t handle—prefer precise constraints.
  • Beware of code bloat from many template instantiations; consider type-erasure/virtual interfaces if needed.
  • Prefer auto and CTAD (Class Template Argument Deduction) when available to reduce verbosity.
  • Document expectations: required operations, complexity notes, and supported types.

Try it yourself

  • Add static_assert inside a template to check constraints (e.g., std::is_arithmetic_v<T>).
  • Experiment with explicit instantiation of a frequently used template to speed up builds.

🔹 FAQs about Templates in C++

Q1: Are templates runtime-polymorphic?
No. They’re compile-time polymorphism. The compiler generates concrete code for the types you use—no vtable overhead.

Q2: Where should I put template definitions?
In headers, so callers can instantiate them. Alternatively, use explicit instantiation (advanced) in a .cpp.

Q3: Can function templates be partially specialized?
No. Only full specialization is allowed for function templates. Class templates support partial specialization.

Q4: What are concepts and why use them?
Concepts (C++20) are readable constraints for templates. They improve error messages and help select the right overloads.

Q5: Do templates increase binary size?
They can due to multiple instantiations. Mitigate with fewer type variants, explicit instantiation, or type-erasure when appropriate.

Q6: Can I specialize a member function of a class template?
Yes, but rules are nuanced. Often you partially specialize the class instead of individual members for clarity.

Q7: What’s SFINAE?
“Substitution Failure Is Not An Error” is a technique to exclude invalid template overloads during substitution. Concepts offer a cleaner alternative.

Q8: How do variadic templates help?
They let you handle a variable number of types/arguments. Fold expressions (C++17) make them concise.

Q9: What is CTAD?
Class Template Argument Deduction lets the compiler deduce template parameters from constructor arguments (e.g., std::pair p(1, 2.0)).

Q10: Templates vs virtual interfaces—when to choose which?
Use templates for performance and when types are known at compile time; use virtual interfaces for runtime pluggability and ABI stability.

🔹 Wrapping Up

Templates in C++ power safe, reusable, and fast code for both functions and classes. With specialization, NTTPs, variadics, and concepts, you can model rich, type-safe APIs without runtime cost. Practice the “Try it yourself” tasks to internalize these patterns and apply them confidently in your projects.

Leave a Comment

About RadiantRiva

Your go-to resource for coding tutorials, developer guides, and programming tips.

Learn More

Quick Links

Follow Us

Newsletter

Get coding tips, tutorials, and updates straight to your inbox.