Lesson 11 of 13

Templates & Compile-Time Generics

Learn to write templates, which let the generate a version of a or for each type you use, with no runtime cost.

C++
#include <iostream>
#include <vector>
#include <string>

// Function template
template <typename T>
T maxOf(const T& a, const T& b) {
    return a > b ? a : b;
}

// Multiple parameters, deduced return type
template <typename A, typename B>
auto add(const A& a, const B& b) -> decltype(a + b) {
    return a + b;
}

// Class template
template <typename T, std::size_t N>
class FixedArray {
    T data_[N]{};
public:
    T& operator[](std::size_t i)             { return data_[i]; }
    const T& operator[](std::size_t i) const { return data_[i]; }
    constexpr std::size_t size() const       { return N; }
    T* begin() { return data_; }
    T* end()   { return data_ + N; }
};

int main() {
    std::cout << maxOf(3, 7) << "\n";                     // int version
    std::cout << maxOf(std::string("a"), std::string("b")) << "\n";  // string version
    std::cout << add(1, 2.5) << "\n";                     // 3.5

    FixedArray<int, 4> arr;
    arr[0] = 10;
    for (int v : arr) std::cout << v << ' ';
}
C++
#include <concepts>
#include <iostream>
#include <vector>

// C++20 concepts: constrain templates and get readable error messages.
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

template <Numeric T>
T mean(const std::vector<T>& values) {
    T total{};
    for (const T& v : values) total += v;
    return values.empty() ? T{} : total / static_cast<T>(values.size());
}

// Custom concept: "anything I can print"
template <typename T>
concept Printable = requires(T t, std::ostream& os) {
    { os << t } -> std::same_as<std::ostream&>;
};

template <Printable T>
void print(const T& value) { std::cout << value << '\n'; }

// Variadic templates: any number of arguments
template <typename... Args>
void printAll(const Args&... args) {
    ((std::cout << args << ' '), ...);      // C++17 fold expression
    std::cout << '\n';
}

int main() {
    std::cout << mean(std::vector<double>{1.0, 2.0, 4.0}) << "\n";   // 2.33
    printAll(1, "two", 3.0, 'x');
    // mean(std::vector<std::string>{});    // clear error: does not satisfy Numeric
}
C++
#include <iostream>
#include <type_traits>

// Compile-time computation with constexpr
constexpr long long factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}
static_assert(factorial(5) == 120);       // checked by the COMPILER

// if constexpr: branches removed at compile time
template <typename T>
std::string describe(const T& value) {
    if constexpr (std::is_integral_v<T>) {
        return "integer: " + std::to_string(value);
    } else if constexpr (std::is_floating_point_v<T>) {
        return "float: " + std::to_string(value);
    } else {
        return "some other type";
    }
}

// Template specialization: a custom implementation for one type
template <typename T> struct TypeName            { static constexpr auto value = "unknown"; };
template <>           struct TypeName<int>       { static constexpr auto value = "int"; };
template <>           struct TypeName<double>    { static constexpr auto value = "double"; };

int main() {
    std::cout << describe(42) << "\n";
    std::cout << describe(3.14) << "\n";
    std::cout << TypeName<int>::value << "\n";
    constexpr auto f = factorial(10);      // computed during compilation
    std::cout << f << "\n";
}
Finished reading?Mark it done to track your progress.

Tip: use the ← and → keys to move between lessons.