C++ Part I · Module 18 · Templates and Generic Programming
Non-Type Template Parameters
Both template forms so far have been parameterised on a type, and for the work this curriculum is aimed at the more useful parameter is often a number. A register is eight or sixteen or thirty-two bits wide. A FIFO has a depth. A bus has a number of lanes, a cache has a number of ways, a packet has a header length. Every one of those is fixed when the code is compiled, which means it does not have to be a constructor argument stored in the object and checked at run time — it can be a template argument, known to the compiler, with three consequences this chapter measures. The storage comes out exact: a thirty-three-bit field occupies five bytes, not a pointer and a length and a heap allocation. The value is a compile-time constant, usable anywhere one is required, including as the bound of a built-in array and inside a static assertion. And each distinct value produces a distinct type, so a thirty-two-bit field and an eight-bit field are no more interchangeable than an int and a string — which turns a class of unit-mismatch bug into a compile error. The chapter also covers what may and may not be used as such a parameter, which is more restrictive than people expect and was relaxed in C++20.
Advanced18 min readC++Part IFundamentalsTemplates
Part I · Module 18 · Chapter 18.5 · Non-Type Template Parameters
1. A Value, Not a Type
template <std::size_t Width>
class BitField { /* ... */ };Width is a non-type template parameter: not a placeholder for a type but for a value, supplied at the point of use and known to the compiler.
BitField<32> status;
BitField<8> flags;The syntax is the same as Chapter 18.4's with a type in place of typename. Everything else about class templates applies unchanged — including Chapter 18.4 §3's distinctness, which §5 shows is the property that earns this its place.
2. The Alternative It Replaces
The run-time version:
class BitField {
public:
explicit BitField(std::size_t width) : width_(width), data_(( width + 7) / 8) {}
void set(std::size_t bit) { if (bit < width_) /* ... */ ; }
private:
std::size_t width_;
std::vector<unsigned char> data_;
};Three costs, and only the first is obvious.
The width is stored in every object — eight bytes to remember a number the programmer knew while typing.
The storage is a heap allocation — a std::vector means a pointer, a size, a capacity, and a call to the allocator.
And the width is not checkable. BitField(32) and BitField(8) are the same type, so nothing stops one being passed where the other was meant. That is the cost §5 is about, and it is the one that produces bugs rather than bytes.
3. The Template Version
template <std::size_t Width>
class BitField {
public:
static constexpr std::size_t width = Width;
static constexpr std::size_t bytes = (Width + 7) / 8;
void set(std::size_t bit) { if (bit < Width) data_[bit/8] |= (unsigned char)(1u << (bit%8)); }
bool get(std::size_t bit) const { return bit < Width && (data_[bit/8] >> (bit%8)) & 1u; }
private:
unsigned char data_[bytes] = {};
};data_ is a built-in array whose bound is computed from the parameter — which is only possible because Width is a compile-time constant. The bit manipulation is Chapter 4.8's.
D. it works: set(5) then get(5)=1 get(6)=04. Exact Storage
A. the width is a compile-time constant, so the storage is exact:
sizeof(BitField<8>) = 1 (bytes=1)
sizeof(BitField<32>) = 4 (bytes=4)
sizeof(BitField<33>) = 5 (bytes=5)One byte for eight bits, four for thirty-two, five for thirty-three. No stored width, no pointer, no allocation, no padding — the object is its data.
Compare §2's version, which for eight bits would be eight bytes of width_ plus a std::vector's three pointers plus a heap block. One byte against roughly thirty-two plus an allocation, for the same capability.
That matters where it matters: a register map with a hundred fields, a packet descriptor in a tight loop, anything in firmware. Chapter 18.4 §5 measured the same property for type parameters — the generated class is laid out as if hand-written.
5. Each Value Is a Distinct Type
B. each WIDTH is a distinct type:
is_same<BitField<8>, BitField<32>> = 0BitField<8> and BitField<32> are unrelated types, exactly as Chapter 18.4 §3 measured for Box<int> and Box<double> — and with the same consequence: no conversion, no assignment, and a function expecting one will not accept the other.
A non-type parameter turns a value into part of the type, which turns a value mismatch into a compile error.
Measured end to end, with a function written for one width and a function written for any:
static void writeStatus(const BitField<32>&); // 32-bit only
template <std::size_t W> void dump(const BitField<W>&); // any width writeStatus(BitField<32>) ok
dump(BitField<32>) ok, bytes=4
dump(BitField<8>) ok, bytes=1and then the mismatch:
writeStatus(f); // an 8-bit field where 32 was required
error: no matching function for call to 'writeStatus'
note: candidate function not viable: no known conversion from 'BitField<8>' to 'const BitField<32> &'no known conversion from 'BitField<8>' to 'const BitField<32> &' — the diagnostic names both widths. For an audience whose bugs include a 32-bit value landing in an 8-bit field, that is the entire argument for this chapter in one error message.
Note the other half of the measurement: dump worked for both widths, and W was available inside it as a constant — so making a width part of the type does not prevent you writing generic code over widths, it just requires that code to be a template. §9 is the cost of that.
The property generalises past bit widths. A Fifo<16> cannot be passed where a Fifo<8> is expected. A Matrix<4,4> cannot be multiplied by a Matrix<3,3> unless you wrote an overload that permits it. Dimensions, widths and depths stop being values that might agree and become part of what the compiler checks.
The cost is the mirror image: two widths are two types, so §9's code-size trade applies, and a function that should accept any width has to be a template itself.
6. The Value Is a Compile-Time Constant
C. the value is usable in a constant expression:
static_assert(BitField<32>::width == 32) passes
int arr[BitField<16>::bytes] -> 2 elementsUsable as an array bound and inside a static assertion — the two places that prove a value is genuinely known at compile time rather than merely constant-looking.
This is what makes §3's unsigned char data_[bytes] legal, and it is what lets you compute with the parameter:
template <std::size_t Width>
struct BitField {
static_assert(Width > 0, "a BitField needs at least one bit");
static_assert(Width <= 4096, "suspiciously wide");
// ...
};Requirements on a value can be stated and enforced, which is a sharp contrast with Chapter 18.2 §4's finding that a type parameter's requirements cannot be expressed before C++20. A value parameter's constraints are checkable today, with a message you wrote.
And Chapter 18.3 §4 measured the deduced form of the same idea:
length(int[7]) = 7 ; length(double[3]) = 3 ; length(char[42]) = 42
int mirror[length(a)] -> 7 elementstemplate <typename T, std::size_t N> f(const T (&)[N]) deduces N — a non-type parameter obtained by deduction rather than written out.
7. What May Be a Non-Type Parameter
More restrictive than people expect, in C++17:
| Allowed | Example |
|---|---|
| integral and enumeration types | template <std::size_t N>, template <Mode M> |
| pointer or reference to an object or function with linkage | template <int* P> |
std::nullptr_t | rare |
bool, char | template <bool Signed> |
| Not allowed in C++17 | Why it matters |
|---|---|
double, float | no floating-point parameters |
std::string | not a literal type |
| class types generally | relaxed in C++20 |
So no floating-point and no string parameters before C++20, which is the restriction people hit first. The workaround for a string tag is an enum, and the workaround for a rational value is two integers:
template <long Num, long Den> struct Ratio { /* ... */ }; // instead of a double(C++20 permits class types with certain properties as non-type parameters, which makes template <FixedString S> possible. That is outside this curriculum's C++17 baseline — Chapter 1.3.)
8. Defaults, and Mixing Kinds
Both ordinary:
template <typename T, std::size_t N> class Array; // mixed
template <std::size_t Width, bool Signed = false> class Field; // a default
template <typename T, std::size_t N = 8> class Fifo; // bothType and non-type parameters mix freely and in any order, and non-type parameters take defaults exactly as type parameters do.
The standard library's own example of this shape is std::array<T, N> — a type parameter and a size parameter, with exact storage and no allocation, which is Module 19's subject. It is worth noticing now that std::array<int, 4> and std::array<int, 8> are distinct types for precisely §5's reason, which is why std::array knows its own size and std::vector has to store one.
9. The Cost
The same trade as Chapter 18.1 §6, with a sharper edge: each distinct value is a separate instantiation.
A BitField<Width> used with eight different widths generates eight classes and eight sets of member functions. Chapter 18.1 §6 measured 180 → 843 bytes for one to eight instantiations of a non-trivial function template, and the arithmetic here is the same.
Three practical consequences.
Prefer a small, fixed set of widths. Eight, sixteen, thirty-two and sixty-four is four instantiations; a width taken from a configuration file with forty possible values is forty.
A width that is genuinely dynamic belongs at run time. If the number is not known until the program reads something, §2's version is the correct design and this chapter's is not available.
And a function that works across widths must be a template, which is fine but worth planning:
template <std::size_t W> void dump(const BitField<W>& f) { /* W is available here */ }10. When to Use Which
| The value is | Use |
|---|---|
| a literal in the source, from a small set | a non-type template parameter |
| a compile-time constant computed from others | a non-type template parameter |
| read from a file, a register, or user input | a constructor argument |
| the same for every object in the program | a constexpr global, or a non-type parameter |
| one of hundreds of possible values | a constructor argument — §9's cost |
The question that decides it: is this value known while the code is being compiled, and does it come from a small set? Both halves matter — the first makes it possible, the second makes it wise.
11. What to Actually Do
Parameterise on a value when it is known at compile time and comes from a small set.
Use it for widths, depths, lane counts and dimensions — the numbers that are fixed by the hardware.
Compute derived constants from it — bytes = (Width + 7) / 8 — and use them as array bounds.
State requirements with static_assert, which works for values where it cannot for types.
Expect each distinct value to cost an instantiation — Chapter 18.1 §6's arithmetic.
Write functions across widths as templates taking const BitField<W>&.
Use a constructor argument when the value is genuinely dynamic.
And remember C++17 forbids floating-point and class-type parameters — use integers or an enum.
12. Mistakes Worth Making Once
Storing a width that was known at compile time. Eight bytes to remember a literal.
Expecting BitField<8> and BitField<32> to interoperate. Measured: is_same = 0.
Trying template <double Scale>. Not permitted in C++17; use two integers.
Trying template <std::string Name>. Not a literal type; use an enum.
Instantiating for a large or open set of values. One class per value — Chapter 18.1 §6.
Using a template parameter for a value read at run time. It cannot be.
Writing a non-template function taking BitField<32> when it should work for any width.
Forgetting that the value is constexpr and checking it at run time instead of with static_assert.
13. Interview Relevance
"What is a non-type template parameter?" A template parameter that is a value rather than a type, known at compile time.
"Why prefer it to a constructor argument?" Exact storage, a compile-time constant usable as an array bound, and — the one that matters — each value becomes a distinct type, so a mismatch is a compile error. Measured: 1, 4 and 5 bytes for widths 8, 32 and 33.
"What can be one in C++17?" Integral and enumeration types, bool, char, pointers and references with linkage — not floating point, not strings, not class types in general.
"How do you express a requirement on it?" static_assert — which works here and cannot work for a type parameter before C++20. A good discriminator.
"What does it cost?" One instantiation per distinct value.
"When should the value stay a constructor argument?" When it is read at run time, or drawn from a large set.
"Where does the standard library use this?" std::array<T, N> — which is why it knows its size without storing one.
14. What You Should Be Able to Say Now
- A non-type template parameter is a value, supplied at the point of use and known to the compiler.
- The run-time alternative stores the value, heap-allocates, and cannot check the width.
- Measured:
sizeof(BitField<8>)=1,<32>=4,<33>=5— exact storage, no stored width and no allocation. - A built-in array bound computed from the parameter is legal, which is what makes that possible.
- Measured:
is_same<BitField<8>, BitField<32>>is 0 — unrelated types, as Chapter 18.4 §3 found for type parameters. - So a width mismatch becomes a compile error rather than a runtime check.
- Measured: the value works in a
static_assertand as an array bound —int arr[BitField<16>::bytes]gave 2 elements. - Requirements on a value parameter are expressible with
static_assert, unlike a type parameter's before C++20. - Measured (Chapter 18.3 §4):
Ncan be deduced —length(char[42])gave 42, usable as an array bound. - C++17 allows integral, enumeration,
bool,char, and pointers/references with linkage. - It forbids floating-point, strings and class types — relaxed in C++20.
- Workarounds: an
enumfor a tag, two integers for a ratio. - Type and non-type parameters mix freely and take defaults.
std::array<T, N>is the library's instance of this shape — and why it needs no stored size.- Each distinct value is an instantiation — Chapter 18.1 §6's 180 → 843 bytes.
- Use it when the value is compile-time known and drawn from a small set; otherwise a constructor argument.
15. Exercises
Exercise 1 — write one. A Fifo<T, Depth> with exact storage and push/pop, with Depth as a non-type parameter.
Exercise 2 — the storage. Predict sizeof for BitField<1>, BitField<16> and BitField<64>, and say what is absent compared with the run-time version.
Exercise 3 — the type check. Write a function that accepts only a 32-bit field, and say what happens when an 8-bit one is passed.
Exercise 4 — across widths. Rewrite that function to accept any width.
Exercise 5 — the constant. Use the parameter as an array bound and in a static_assert, and say what each proves.
Exercise 6 — the requirement. Add two static_asserts to your Fifo and say why the same technique is unavailable for a type parameter in C++17.
Exercise 7 — what is allowed. For double, std::size_t, std::string, an enum, and bool, say which may be non-type parameters in C++17 and give a workaround for each that may not.
Exercise 8 — the cost. A field width read from a config file with 40 possible values. Say what happens if you make it a template parameter and what you should do instead.
16. Summary
- A non-type template parameter is a compile-time value — a width, a depth, a size.
- Measured:
BitField<8>,<32>and<33>occupy exactly 1, 4 and 5 bytes — no stored width, no allocation. - Measured: each width is a distinct type (
is_same= 0), so a mismatch is a compile error rather than a check. - Measured: the value is usable in a
static_assertand as an array bound. - So requirements on a value parameter are expressible today, unlike a type parameter's before C++20.
- C++17 allows integral, enumeration,
bool,charand linked pointers/references; not floating-point, strings or class types. - Type and non-type parameters mix and take defaults —
std::array<T, N>is the library's example. - Each distinct value costs an instantiation — Chapter 18.1 §6's arithmetic.
- Use it when the value is compile-time known and from a small set; otherwise keep it a constructor argument.
Three chapters have written templates and two have measured what they produce, and the module's frozen blurb says the emphasis belongs on a single idea that has been invoked four times without being examined. Chapter 18.1 §7 stated it in one line — a template is not code until a type is supplied. Chapter 18.2 §8 relied on it to explain why an error is reported inside a template and blamed on a call site. Chapter 18.4 §7 measured a member function containing outright nonsense compiling because nobody called it. The next chapter is that idea properly, and it turns out to be sharper than the one-line version: some errors in a template nobody instantiates are caught anyway, and the rule separating the two cases is precise, measurable, and explains both the error messages and the header organisation that the two chapters after it are about.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
