Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 5 of 8

A template parameter does not have to be a type. It can be a value, known at compile time.

That is what makes std::array<int, 5> different from std::vector<int>: the 5 is part of the type.

The syntax

template <typename T, std::size_t N>
class FixedArray { /* ... */ };

typename T introduces a type; std::size_t N introduces a value. The value must be a compile- time constant — a literal, a constexpr, or another template parameter. A run-time variable will not do.

A fixed-size array

#include <iostream>
#include <stdexcept>

// N is a value, not a type. It must be known at compile time.
template <typename T, std::size_t N>
class FixedArray {
    T items[N] = {};

public:
    T& operator[](std::size_t i) { return items[i]; }
    const T& operator[](std::size_t i) const { return items[i]; }

    T& at(std::size_t i) {
        if (i >= N) {
            throw std::out_of_range("FixedArray::at index out of range");
        }
        return items[i];
    }

    // N is a compile-time constant, so size() is too.
    static constexpr std::size_t size() { return N; }
};

int main() {
    FixedArray<int, 5> a;
    for (std::size_t i = 0; i < a.size(); ++i) {
        a[i] = static_cast<int>(i * i);
    }
    for (std::size_t i = 0; i < a.size(); ++i) {
        std::cout << a[i] << " ";
    }
    std::cout << "\n";

    std::cout << "sizeof(FixedArray<int, 5>)   = " << sizeof(FixedArray<int, 5>) << "\n";
    std::cout << "sizeof(FixedArray<int, 100>) = " << sizeof(FixedArray<int, 100>) << "\n";

    // The size is part of the type, so this is checked at compile time:
    static_assert(FixedArray<int, 5>::size() == 5);

    try {
        a.at(99);
    } catch (const std::out_of_range& e) {
        std::cout << "caught: " << e.what() << "\n";
    }
    return 0;
}
0 1 4 9 16 
sizeof(FixedArray<int, 5>)   = 20
sizeof(FixedArray<int, 100>) = 400
caught: FixedArray::at index out of range

Read the sizeof line. There is no pointer and no heap allocation — the elements live inside the object, so a FixedArray<int, 5> is exactly five ints. Compare Vector<T> from lesson 12.3, which is 24 bytes regardless of how much it holds, with the elements on the heap.

That is the real trade. std::array is stack-allocated and its size is fixed at compile time; std::vector is heap-allocated and can grow. Neither is better — they answer different questions.

static_assert(FixedArray<int, 5>::size() == 5) is checked while compiling. If it were false the build would fail, with no program to run.

What can be a non-type parameter

Since C++20, more than you might expect:

#include <iostream>

// C++20 allows a floating-point non-type parameter, and a class type too.
template <double Scale>
double apply(double x) {
    return x * Scale;
}

struct Point {
    int x;
    int y;
};

template <Point P>
void show() {
    std::cout << "compile-time point (" << P.x << ", " << P.y << ")\n";
}

int main() {
    std::cout << apply<2.5>(4.0) << "\n";
    show<Point{3, 4}>();
    return 0;
}
10
compile-time point (3, 4)

Both of those are C++20 features. Compiled as C++17 the same file gives:

error: 'double' is not a valid type for a template non-type parameter
error: non-type template parameters of class type only available with '-std=c++20'

The permitted kinds, in order of how often you will see them:

  • Integers and bool — by far the most common, and the only kind before C++20.
  • char and enumerations — useful for tagging.
  • Pointers and references to objects with static storage — rare.
  • Floating point (C++20).
  • Literal class types (C++20) — a struct of constant members, as above.

What is not allowed: std::string, or anything requiring a heap allocation. A compile-time string has to be a const char* or a fixed character array.

Compile-time computation

Because the parameter is a constant, the compiler can compute with it:

#include <array>
#include <iostream>
#include <string>

// A compile-time computation: the value is produced while compiling.
template <int N>
struct Factorial {
    static constexpr long long value = N * Factorial<N - 1>::value;
};

template <>
struct Factorial<0> {
    static constexpr long long value = 1;
};

// Units checked by the type system: metres and feet cannot be added.
template <int MetreExponent>
class Length {
    double amount;

public:
    explicit constexpr Length(double a) : amount(a) {}
    constexpr double value() const { return amount; }
};

using Metres = Length<1>;
using SquareMetres = Length<2>;

constexpr Metres operator+(Metres a, Metres b) { return Metres(a.value() + b.value()); }
constexpr SquareMetres operator*(Metres a, Metres b) { return SquareMetres(a.value() * b.value()); }

int main() {
    // Computed at compile time — the binary contains the number, not the loop.
    std::cout << "10! = " << Factorial<10>::value << "\n";
    static_assert(Factorial<5>::value == 120);

    Metres width(3.0);
    Metres height(4.0);
    std::cout << "perimeter part " << (width + height).value() << " m\n";
    std::cout << "area " << (width * height).value() << " m^2\n";

    // Adding a Metres to a SquareMetres does not compile — the exponents differ,
    // so they are different types.

    // std::array is exactly this: template <typename T, std::size_t N>
    std::array<int, 4> fixed = {1, 2, 3, 4};
    std::cout << "std::array size " << fixed.size()
              << ", sizeof " << sizeof(fixed) << "\n";
    return 0;
}
10! = 3628800
perimeter part 7 m
area 12 m^2
std::array size 4, sizeof 16

Two patterns there, both worth knowing.

Factorial<N> is recursion in the type system: the general template refers to Factorial<N-1>, and the specialization at 0 stops it. The result is a constant in the binary — there is no loop at run time. This is template metaprogramming, and it was the only way to compute at compile time before C++11. Today, write a constexpr function instead; it reads like ordinary code and does the same job. The pattern is here because you will meet it in older libraries.

Length<Exponent> encodes units in the type. Metres and SquareMetres are different types, so width + area does not compile. width * height returns SquareMetres because the operator says so. A whole class of unit-mixing bugs — the kind that has lost spacecraft — becomes impossible, at zero run-time cost, because the exponent exists only while compiling.

The cost: one instantiation per value

Each distinct value produces a separate instantiation, exactly as each distinct type does:

#include <array>
#include <cstddef>
#include <iostream>

template <std::size_t N>
int sum_of(const std::array<int, N>& values) {
    int total = 0;
    for (std::size_t i = 0; i < N; ++i) {
        total += values[i];
    }
    return total;
}

int main() {
    std::array<int, 3> a = {1, 2, 3};
    std::array<int, 4> b = {1, 2, 3, 4};
    std::array<int, 5> c = {1, 2, 3, 4, 5};
    std::cout << sum_of(a) << " " << sum_of(b) << " " << sum_of(c) << "\n";
    return 0;
}
6 10 15

Three sizes, three functions in the binary:

$ nm -C ./sums | grep sum_of
00000000000012a6 W int sum_of<3ul>(std::array<int, 3ul> const&)
00000000000012f0 W int sum_of<4ul>(std::array<int, 4ul> const&)
000000000000133a W int sum_of<5ul>(std::array<int, 5ul> const&)

That is fine for three sizes. Instantiate a large class template over fifty different sizes and you get fifty copies of every member — the binary grows, and compilation slows. This is template code bloat, and non-type parameters cause more of it than type parameters do, because values vary more than types do.

The usual fix is to put the size-independent work in a non-template function taking a pointer and a length (or, better, a std::span — lesson 13.11), and keep only a thin size-aware wrapper as the template.

std::array versus std::vector

| | std::array<T, N> | std::vector<T> | |---|---|---| | Size known | at compile time | at run time | | Storage | in the object (stack, if the object is) | heap | | Can grow | no | yes | | sizeof | N * sizeof(T) | fixed, usually 24 | | Size is part of the type | yes | no | | Passing to a function | size is in the type | size is a member |

Use std::array when the size is genuinely fixed and known — a 3D coordinate, a lookup table, a fixed-size buffer. Use std::vector otherwise. The compile-time size buys you no allocation and better optimisation, at the cost of a separate instantiation per size.

Try it

Write Matrix<T, Rows, Cols> with operator()(row, col) access, a transpose() returning Matrix<T, Cols, Rows>, and multiplication that only compiles when the dimensions agree. Prove the dimension check works by writing a multiplication that should be rejected.

Checkpoint

  • A template parameter can be a value, not just a type: std::array<int, 5>.
  • The value must be a compile-time constant.
  • Integers, bool, char, and enums have always worked; C++20 adds floats and literal classes.
  • std::string cannot be a non-type parameter.
  • The size becomes part of the type, so it can be checked with static_assert.
  • Encoding units or dimensions in the type makes whole bug classes impossible for free.
  • Factorial<N> shows compile-time recursion — real, but write a constexpr function instead.
  • Each distinct value creates a separate instantiation, so watch for code bloat.
  • std::array is compile-time sized and stack-allocated; std::vector is neither.

Questions about this lesson

Log in to ask a question.