Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 9 – Templates and concepts

Non-type and template template parameters

Give templates values such as sizes and string literals as parameters, see why each value makes a new type, and pass std::vector as a template argument.

  • Intermediate
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Write a class template whose capacity is a value parameter, and check it with static_assert
  • Explain why two values of a parameter make two unrelated types, and deduce a value from an argument
  • Choose a type for a value parameter, including auto, floating point and class types
  • Use a C++20 FixedString class parameter to carry a string literal into a type
  • Pass a container template such as std::vector as a template argument

Before you start

What you’ll learn · Before you start

What you will learn

  • Write a class template whose capacity is a value parameter, and check it with static_assert
  • Explain why two values of a parameter make two unrelated types, and deduce a value from an argument
  • Choose a type for a value parameter, including auto, floating point and class types
  • Use a C++20 FixedString class parameter to carry a string literal into a type
  • Pass a container template such as std::vector as a template argument

Before you start

On this page

A FixedStack<int, 8> is 40 bytes, a FixedStack<int, 16> 72, and neither allocates memory: their capacities, 8 and 16, are template arguments, values the compiler builds into each type. A non-type template parameter: A template parameter that holds a value instead of a type, such as N in template <typename T, std::size_t N>. Its argument must be a constant expression, and each value makes a different type: std::array<int, 5> is not std::array<int, 6>. is that kind of parameter, a value such as the 5 in std::array<int, 5> rather than a type.

You will build a fixed-capacity stack, measure what its capacity does to the object’s size, find the values in compiled function names, turn string literals into types with a FixedString class, and pass std::vector itself to a template that supplies its arguments. Every example is compiled with Apple clang 21 and GCC 16.

A value can be a template parameter

FixedStack<T, N> stores up to N elements of type T in a std::array<T, N> inside the object. std::size_t N in the template line declares a value parameter, and N can be used in the class like a constant: as the array’s size, in a static_assert, in comparisons.

A stack of three moves, and four sizes C++ · fixed_stack.cpp
// A stack whose capacity N is a template parameter: a value known at compile time, stored in the type.
#include <array>
#include <cstddef>
#include <print>
#include <vector>

template <typename T, std::size_t N>
class FixedStack {
    static_assert(N > 0, "a FixedStack holds at least one element");

public:
    bool push(const T& value) {  // false when the stack is full
        if (count_ == N) return false;
        items_[count_++] = value;
        return true;
    }
    const T& top() const { return items_[count_ - 1]; }  // only for a non-empty stack
    std::size_t size() const { return count_; }
    static constexpr std::size_t capacity() { return N; }

private:
    std::array<T, N> items_{};  // N elements inside the object: no heap allocation
    std::size_t count_ = 0;
};

int main() {
    FixedStack<int, 3> moves;
    for (int m : {7, 4, 9, 2}) {
        bool ok = moves.push(m);
        std::println("push({}) -> {}", m, ok ? "stored" : "full");
    }
    std::println("top {}, size {} of {}", moves.top(), moves.size(), moves.capacity());

    std::println("sizeof(FixedStack<int, 8>)     = {}", sizeof(FixedStack<int, 8>));
    std::println("sizeof(FixedStack<int, 16>)    = {}", sizeof(FixedStack<int, 16>));
    std::println("sizeof(FixedStack<double, 16>) = {}", sizeof(FixedStack<double, 16>));
    std::println("sizeof(std::vector<int>)       = {}", sizeof(std::vector<int>));  // its elements live elsewhere
}

Output

push(7) -> stored
push(4) -> stored
push(9) -> stored
push(2) -> full
top 9, size 3 of 3
sizeof(FixedStack<int, 8>)     = 40
sizeof(FixedStack<int, 16>)    = 72
sizeof(FixedStack<double, 16>) = 136
sizeof(std::vector<int>)       = 24

Recorded with Apple clang version 21.0.0 (clang-2100.3.34.2) on macOS 26 arm64. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined fixed_stack.cpp -o fixed_stack && ./fixed_stack

Step through the loop of the program and watch the four pushes into a stack whose N is 3:

Interactive · step by step Four pushes into a FixedStack<int, 3> Step through the loop of fixed_stack.cpp. The three slots are the std::array<int, 3> inside the object; count_ is the other member.
Four pushes into a FixedStack<int, 3>, step 6 of 6. Showing items_[0] (7), items_[1] (4), items_[2] (9), count_ (3 of N = 3), push(value) (3 == 3? yes), What it returns (false: full). In focus: items_[2].comparesitems_[0]7items_[1]4items_[2]9count_3 of N = 3push(value)3 == 3? yesWhat it returnsfalse: full

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. An empty stack. FixedStack<int, 3> moves; holds its three slots inside the object, a std::array<int, 3>, and a count that starts at 0. N is 3 in this type, fixed while compiling.
  2. push(7). count_ is 0, not N, so 7 goes into items_[0] and count_ becomes 1. push returns true, and the program printed push(7) -> stored.
  3. push(4). 4 goes into items_[1], and count_ becomes 2.
  4. push(9). 9 fills items_[2], the last slot, and count_ becomes 3, which is N.
  5. push(2) is refused. Now count_ == N, so push returns false and changes nothing: the program printed push(2) -> full. No memory is allocated to make room, because the capacity is part of the type.
  6. After the loop. top() reads items_[count_ - 1], 9, and the program printed top 9, size 3 of 3. capacity() returns N itself, a constant of the type.

The fourth push is refused, because count_ == N and N is 3 for this type. The sizes show where the elements live:

Type sizeof Where the elements are
FixedStack<int, 8> 40 bytes Eight 4-byte ints and an 8-byte count, inside the object
FixedStack<int, 16> 72 bytes Sixteen ints and the count, inside the object
FixedStack<double, 16> 136 bytes Sixteen 8-byte doubles and the count
std::vector<int> 24 bytes On the heap, however many there are

A fixed capacity suits small buffers that are created often or that must not allocate, such as code running inside an interrupt handler or a game’s inner loop, while a vector suits sizes known only at run time. The static_assert rejects FixedStack<int, 0> at compile time, which is possible only because N is a constant expression: An expression the compiler can evaluate while it compiles, such as 6 * 7 or a call of a constexpr function with constant arguments. Array sizes, template arguments, case labels and static_assert need one, and one may never contain undefined behaviour. More in “C++ const, constexpr, consteval and constinit”, known while compiling.

Checkpoint

Check: Values as template parameters

  1. Question 1 of 2 Which of these can be the argument of FixedStack<int, N>?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: sizeof(long) * 4

    A non-type template argument must be a constant expression, a value the compiler can work out while compiling. sizeof(long) * 4 is one; values that exist only when the program runs are not.

  2. Question 2 of 2 FixedStack<int, N> holds a std::array<int, N> and a std::size_t, and sizeof(FixedStack<int, 8>) was 40 on both compilers. What is sizeof(FixedStack<int, 32>)?

    Type a number.

    A hint

    Work out the 40 first: how many bytes are elements, and how many the count?

    Show the answer: Question 2 of 2

    Answer: 136 bytes

    32 ints of 4 bytes are 128 bytes, plus 8 bytes for the count: 136. The capacity is stored in the type, so the object holds only the elements and the count.

Each value makes a different type

The value is part of the type’s name. A function template can therefore deduce it from an argument, as capacity_of does below, and length_of deduces the length of a plain array from a reference to it. The compiled function names show the values, and assigning one size to the other is rejected:

Deducing N, and an assignment between two sizes

capacity_of.cpp

// A function template can deduce a value parameter, here N, from the type of its argument.
#include <array>
#include <cstddef>
#include <print>

template <typename T, std::size_t N>
struct Buffer {
    std::array<T, N> data{};
};

template <typename T, std::size_t N>
std::size_t capacity_of(const Buffer<T, N>&) {
    return N;
}

template <typename T, std::size_t N>
constexpr std::size_t length_of(const T (&)[N]) {  // a reference to an array keeps its size
    return N;
}

int main() {
    Buffer<int, 8> small;
    Buffer<int, 16> large;
    int marks[5] = {72, 91, 64, 85, 58};
    std::println("{} {} {}", capacity_of(small), capacity_of(large), length_of(marks));
}

assign.cpp

// Buffer<int, 8> and Buffer<int, 16> are two unrelated types: one cannot be assigned to the other.
#include <array>
#include <cstddef>

template <typename T, std::size_t N>
struct Buffer {
    std::array<T, N> data{};
};

int main() {
    Buffer<int, 8> small;
    Buffer<int, 16> large;
    small = large;
}
The values in the function names, then the refused assignment Python · capacity_session.py
from session import run

# Run capacity_of.cpp, list the instantiations in its object file, then try the assignment in assign.cpp.
run("""
clang++ -std=c++23 -Wall -Wextra capacity_of.cpp -o capacity_of && ./capacity_of
clang++ -std=c++23 -c capacity_of.cpp -o capacity_of.o && nm -C -j capacity_of.o | grep -E 'capacity_of|length_of'
g++-16 -std=c++23 -c capacity_of.cpp -o capacity_of-gcc.o && nm -C -j capacity_of-gcc.o | grep -E 'capacity_of|length_of'
clang++ -std=c++23 -c assign.cpp
g++-16 -std=c++23 -c assign.cpp
""")

Output

$ clang++ -std=c++23 -Wall -Wextra capacity_of.cpp -o capacity_of && ./capacity_of
8 16 5
$ clang++ -std=c++23 -c capacity_of.cpp -o capacity_of.o && nm -C -j capacity_of.o | grep -E 'capacity_of|length_of'
unsigned long capacity_of<int, 16ul>(Buffer<int, 16ul> const&)
unsigned long capacity_of<int, 8ul>(Buffer<int, 8ul> const&)
unsigned long length_of<int, 5ul>(int const (&) [5ul])
$ g++-16 -std=c++23 -c capacity_of.cpp -o capacity_of-gcc.o && nm -C -j capacity_of-gcc.o | grep -E 'capacity_of|length_of'
unsigned long capacity_of<int, 16ul>(Buffer<int, 16ul> const&)
unsigned long capacity_of<int, 8ul>(Buffer<int, 8ul> const&)
unsigned long length_of<int, 5ul>(int const (&) [5ul])
$ clang++ -std=c++23 -c assign.cpp
assign.cpp:13:11: error: no viable overloaded '='
   13 |     small = large;
      |     ~~~~~ ^ ~~~~~
assign.cpp:6:8: note: candidate function (the implicit copy assignment operator) not viable: no known conversion from 'Buffer<[...], 16>' to 'const Buffer<[...], 8>' for 1st argument
    6 | struct Buffer {
      |        ^~~~~~
assign.cpp:6:8: note: candidate function (the implicit move assignment operator) not viable: no known conversion from 'Buffer<[...], 16>' to 'Buffer<[...], 8>' for 1st argument
    6 | struct Buffer {
      |        ^~~~~~
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -c assign.cpp
assign.cpp: In function 'int main()':
assign.cpp:13:13: error: no match for 'operator=' (operand types are 'Buffer<int, 8>' and 'Buffer<int, 16>')
   13 |     small = large;
      |             ^~~~~
  • there are 2 candidates
    • candidate 1: 'constexpr Buffer<int, 8>& Buffer<int, 8>::operator=(const Buffer<int, 8>&)'
      assign.cpp:6:8:
          6 | struct Buffer {
            |        ^~~~~~
      • no known conversion for argument 1 from 'Buffer<int, 16>' to 'const Buffer<int, 8>&'
    • candidate 2: 'constexpr Buffer<int, 8>& Buffer<int, 8>::operator=(Buffer<int, 8>&&)'
      • no known conversion for argument 1 from 'Buffer<int, 16>' to 'Buffer<int, 8>&&'
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 capacity_session.py

capacity_of<int, 8ul> and capacity_of<int, 16ul> are two functions in the object file, just as larger<int> and larger<double> were in the function templates lesson. length_of<int, 5ul> worked out the array’s length from its type, int[5], which a by-value parameter would have lost when the array decayed to a pointer. The assignment fails with “no viable overloaded ‘=’” on Clang and “no match for ‘operator=’” on GCC, since Buffer<int, 8> has no assignment from Buffer<int, 16>. Code that should accept every size has to be a template over N too, or take the elements through a view whose length is a run-time value, such as std::span: A view of a contiguous sequence of elements that it does not own (C++20, header <span>): a pointer to the first element and their number. It is made from a C array, a std::array or a std::vector, and sliced without copying. More in “Pointer arithmetic, arrays and std::span”.

Key idea

A value parameter is part of the type: Buffer<int, 8> and Buffer<int, 16> are as unrelated as two classes with different names, and code for every size is a template over N.

Checkpoint

Check: One type per value

  1. Question 1 of 2 What is true of FixedStack<int, 8> and FixedStack<int, 16>?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: They are unrelated types, so one cannot be assigned to the other.

    Each value of N makes a separate class with its own member functions; the compilers reject small = large with "no viable overloaded '='" (Clang) and "no match for 'operator='" (GCC).

  2. Question 2 of 2 length_of(const T (&)[N]) returns N. What does it return for double readings[7];?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 7 elements

    The reference keeps the array type double[7], so deduction finds T = double and N = 7. N counts elements, not bytes: sizeof(readings) would be 56.

A value parameter has one of a few kinds of type

A non-type template parameter can have one of these types:

  • an integer type, bool, a character type or an enumeration;
  • a pointer, a pointer to member or a reference, whose argument must refer to an object or function with static storage duration: The lifetime of a global, a variable at namespace scope or a static variable: it exists for the whole run of the program. Variables with thread storage duration, declared thread_local, exist once per thread instead. More in “C++ const, constexpr, consteval and constinit” (a pointer may also be null), or std::nullptr_t;
  • since C++20, a floating-point type, and a structural type: A type that may be the type of a non-type template parameter: a scalar type such as int, double or a pointer, an lvalue reference, or (C++20) a literal class whose bases and non-static data members are all public, not mutable and structural themselves. class type: a literal class whose bases and data members are all public, not mutable, and themselves structural.

template <auto V> (C++17, paper P0127R2) leaves the type open: the parameter takes the type of its argument, just as auto x = …; would.

Four values of four types, and two floating-point traps C++ · auto_param.cpp
// template <auto V> (C++17) takes a value of any type allowed for a template parameter, and deduces that type.
#include <print>
#include <type_traits>
#include "type_name.h"

template <auto V>
struct Constant {
    static constexpr auto value = V;
};

enum class Level { low, high };

// A floating-point value as a template argument: allowed since C++20.
template <double Rate>
double with_rate(double amount) {
    return amount * (1.0 + Rate);
}

int main() {
    std::println("Constant<5>           value type {}", type_name<decltype(Constant<5>::value)>());
    std::println("Constant<'x'>         value type {}", type_name<decltype(Constant<'x'>::value)>());
    std::println("Constant<2.5>         value type {}", type_name<decltype(Constant<2.5>::value)>());
    std::println("Constant<Level::high> value type {}", type_name<decltype(Constant<Level::high>::value)>());
    std::println("with_rate<0.18>(250.0) = {:.2f}", with_rate<0.18>(250.0));

    // Two floating-point arguments give the same type only if their values are identical, bit for bit.
    std::println("Constant<0.1 + 0.2> is Constant<0.3>? {}", std::is_same_v<Constant<0.1 + 0.2>, Constant<0.3>>);
    std::println("Constant<0.0> is Constant<-0.0>?      {}", std::is_same_v<Constant<0.0>, Constant<-0.0>>);
}

Output

Constant<5>           value type const int
Constant<'x'>         value type const char
Constant<2.5>         value type const double
Constant<Level::high> value type const Level
with_rate<0.18>(250.0) = 295.00
Constant<0.1 + 0.2> is Constant<0.3>? false
Constant<0.0> is Constant<-0.0>?      false

Recorded with Apple clang version 21.0.0 (clang-2100.3.34.2) on macOS 26 arm64. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined auto_param.cpp -o auto_param && ./auto_param

Constant<5>, Constant<'x'>, Constant<2.5> and Constant<Level::high> deduced int, char, double and Level. Floating-point arguments come with a rule that surprises people: two of them make the same type only when their values are identical, bit for bit. 0.1 + 0.2 is not exactly 0.3 in binary floating point, so Constant<0.1 + 0.2> and Constant<0.3> are different types, and so are Constant<0.0> and Constant<-0.0>, although 0.0 == -0.0 is true.

The kinds of value parameter grew in two editions. Walk through what each change added, and which example of this lesson uses it:

Timeline · 4 events in order What C++17 and C++20 added to template parameters Walk through the four changes in order. Each one names its paper, the compiler versions that support it and the example of this lesson that uses it.
  1. C++17: auto

    auto for a value parameter new syntax

    template <auto V> takes its type from the argument, as Constant<5>, Constant<'x'> and Constant<Level::high> did (paper P0127R2; GCC 7, Clang 4).

  2. C++17: matching

    Defaults count when a template is matched

    template <typename> class Container accepts std::vector, whose second parameter has a default, as Inbox does (paper P0522R0; GCC 7, and Clang 19 by default).

  3. C++20: classes

    Class types as value parameters new syntax

    A structural class such as FixedString can be a template argument, which is how a string literal becomes part of Field's type (paper P0732R2; GCC 9, Clang 12).

  4. C++20: floats

    Floating-point values

    with_rate<0.18> takes a double; two arguments make one type only when identical bit for bit (paper P1907R1; complete in GCC 11; Clang 18, except one corner case with references).

The timeline has 4 events, from C++17: auto to C++20: floats.

Expecting equal values to make one type

Constant<0.1 + 0.2> and Constant<0.3> printed false: the two doubles differ in their last bit. For a parameter such as a rate, take an integer number of basis points instead, 1,800 for the 0.18 of with_rate, so that one value always means one type.

Checkpoint

Check: The types a value parameter may have

  1. Question 1 of 2 Given template <auto V> struct Constant { static constexpr auto value = V; };, what is the type of Constant<'x'>::value?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: const char

    template <auto V> deduces the parameter's type from the argument, here char, and a static constexpr member is const. Both compilers printed const char in the lesson's run.

  2. Question 2 of 2 With template <auto V> struct Constant, which pair names one type?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Constant<1 + 1> and Constant<2>

    Two value arguments make the same type when they have the same type and, for floating point, the same bits. 1 + 1 is the int 2, so Constant<1 + 1> is Constant<2>.

A class type carries a string literal into a type

A pointer parameter cannot take a string literal. The standard forbids a template argument that points to a string literal object, so Tag<"price"> fails for template <const char* Name>, while a named array with static storage, such as static constexpr char price_name[] = "price";, is accepted. C++20 offers a neater route: a structural class that copies the characters of the literal into a public array, so that the literal becomes a constructor argument of a class-type parameter:

A pointer parameter refuses the literal; a FixedString parameter takes it

literal_param.cpp

// A pointer parameter cannot take a string literal: the standard forbids pointing a template argument at one.
template <const char* Name>
struct Tag {};

int main() {
    Tag<"price"> price_tag;
    (void)price_tag;
}

fixed_string.cpp

// C++20: a class-type value as a template argument, so a string literal can become part of a type.
#include <algorithm>
#include <cstddef>
#include <print>
#include <string_view>
#include <type_traits>

template <std::size_t N>
struct FixedString {
    char text[N]{};  // a public array of char: FixedString is a structural type, so it can be a template argument
    constexpr FixedString(const char (&s)[N]) { std::copy_n(s, N, text); }
    constexpr std::string_view view() const { return {text, N - 1}; }
};

template <FixedString Name, typename T>
struct Field {
    T value{};
    static constexpr std::string_view name() { return Name.view(); }
};

template <typename F>
void print_field(const F& field) {
    std::println("{} = {}", F::name(), field.value);
}

int main() {
    Field<"price", double> price{249.5};
    Field<"quantity", int> quantity{3};
    print_field(price);
    print_field(quantity);
    static_assert(!std::is_same_v<Field<"price", double>, Field<"Price", double>>);  // another name, another type
    std::println("sizeof(price) = {}", sizeof(price));  // the name lives in the type, not in the object
}
Two refusals, then the field names as types Python · strings_session.py
from session import run

run("""
clang++ -std=c++23 -c literal_param.cpp
g++-16 -std=c++23 -c literal_param.cpp
clang++ -std=c++23 -Wall -Wextra fixed_string.cpp -o fixed_string && ./fixed_string
g++-16 -std=c++23 -Wall -Wextra fixed_string.cpp -o fixed_string-gcc && ./fixed_string-gcc
clang++ -std=c++23 -c fixed_string.cpp -o fixed_string.o && nm -C -j fixed_string.o | grep print_field
""")

Output

$ clang++ -std=c++23 -c literal_param.cpp
literal_param.cpp:6:9: error: pointer to subobject of string literal is not allowed in a template argument
    6 |     Tag<"price"> price_tag;
      |         ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -c literal_param.cpp
literal_param.cpp: In function 'int main()':
literal_param.cpp:6:16: error: '"price"' is not a valid template argument for type 'const char*' because string literals can never be used in this context
    6 |     Tag<"price"> price_tag;
      |                ^
[exit status 1]
$ clang++ -std=c++23 -Wall -Wextra fixed_string.cpp -o fixed_string && ./fixed_string
price = 249.5
quantity = 3
sizeof(price) = 8
$ g++-16 -std=c++23 -Wall -Wextra fixed_string.cpp -o fixed_string-gcc && ./fixed_string-gcc
price = 249.5
quantity = 3
sizeof(price) = 8
$ clang++ -std=c++23 -c fixed_string.cpp -o fixed_string.o && nm -C -j fixed_string.o | grep print_field
void print_field<Field<FixedString<6ul>{"price"}, double>>(Field<FixedString<6ul>{"price"}, double> const&)
void print_field<Field<FixedString<9ul>{"quantity"}, int>>(Field<FixedString<9ul>{"quantity"}, int> const&)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 strings_session.py

In template <FixedString Name, typename T>, the parameter’s type is the class template FixedString with no arguments, so class template argument deduction (CTAD): The C++17 rule that lets a declaration such as std::pair p{1, 2.0} leave out a class template's arguments: the compiler deduces them from the initialiser, treating each constructor and deduction guide as a function template. More in “Class templates and argument deduction (CTAD)” works out N from the literal. Step through what happens to "price":

Interactive · step by step How the literal "price" becomes part of a type Step through what the compiler does with Field<"price", double>. The left column is the template argument; the right one is what the program ends up with.
How the literal "price" becomes part of a type, step 5 of 5. Showing "price", FixedString Name, N is deduced (N = 6: FixedString<6>), The argument (FixedString<6>{"price"}), Field<…, double> (the name is in the type), Field object price (8 bytes: one double). In focus: Field object price.matched against"price"a const char[6]FixedString Namea class-type parameterN is deducedN = 6: FixedString<6>The argumentFixedString<6>{"price"}Field<…, double>the name is in the typeField object price8 bytes: one double

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. A literal, a class parameter. The parameter is template <FixedString Name, …>: its type is the class template FixedString, written without arguments. The argument is the literal "price", 5 letters and the final zero.
  2. CTAD finds N. Class template argument deduction runs on the constructor FixedString(const char (&s)[N]). A reference keeps the array whole, so N is 6.
  3. The value is made. The constructor copies the six characters into the public array text. That object, FixedString<6>{"price"}, is the template argument: a value the compiler keeps.
  4. A type with a name inside. Field<"price", double> is now a type of its own. nm -C showed the text in its symbol, Field<FixedString<6ul>{"price"}, double>, and "Price" would make another type.
  5. Nothing in the object. An object of the type holds only its double, so sizeof(price) printed 8. F::name() reads the name from the type, and print_field printed price = 249.5.

N is 6 for "price", with its terminating zero. Each name makes its own type, so Field<"price", double> and Field<"Price", double> differ, as the static_assert proves, and the symbol names show the text, FixedString<6ul>{"price"}, inside the type. The object holds only the double: sizeof(price) is 8. The name takes no room in any object, code can select or compare fields by name while compiling, and print_field printed it from the type alone.

Checkpoint

Check: String literals through a class parameter

  1. Question 1 of 2 Why does Tag<"price"> not compile when Tag is declared template <const char* Name> struct Tag;?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The standard does not allow a template argument to point to a string literal.

    Pointer parameters are allowed, but not a pointer to a string literal object. A named array with static storage works, and since C++20 a class type such as FixedString can take the literal as a constructor argument.

  2. Question 2 of 2 What is N in the FixedString<N> of Field<"quantity", int>?

    Type a number.

    A hint

    Count the characters the literal holds, the hidden one too.

    Show the answer: Question 2 of 2

    Answer: 9

    "quantity" has 8 letters and the terminating zero, so it is a const char[9], and deduction through the constructor's array reference finds N = 9. nm -C showed FixedString<9ul>{"quantity"}.

A template template parameter takes a template

Sometimes a template should let its user choose a container, not an element type. A template template parameter: A template parameter that takes a template, such as template <typename...> class Container, so that a call can pass std::vector itself and the template writes Container<int> with arguments of its own choosing. declares a parameter that is itself a template: template <typename...> class Container accepts any class template whose parameters are all types, and the code then writes Container<int> with arguments of its own choosing.

container_param.cpp C++ · container_param.cpp
// A template template parameter takes a template, such as std::vector, and supplies its arguments itself.
#include <deque>
#include <list>
#include <print>
#include <utility>
#include <vector>
#include "type_name.h"

// Container is a template with any number of type parameters; squares() fills a Container<int>.
template <template <typename...> class Container>
Container<int> squares(int n) {
    Container<int> out;
    for (int i = 1; i <= n; ++i) out.push_back(i * i);
    return out;
}

// One type parameter is enough to accept std::vector, whose allocator parameter has a default (C++17).
template <typename T, template <typename> class Container = std::vector>
class Inbox {
public:
    void add(T item) { items_.push_back(std::move(item)); }
    std::size_t size() const { return items_.size(); }

private:
    Container<T> items_;
};

int main() {
    auto v = squares<std::vector>(4);
    auto d = squares<std::deque>(4);
    auto l = squares<std::list>(4);
    std::println("{} {}", type_name<decltype(v)>(), v);
    std::println("{} {}", type_name<decltype(d)>(), d);
    std::println("{} {}", type_name<decltype(l)>(), l);

    Inbox<int> plain;                  // a std::vector<int> inside
    Inbox<int, std::deque> queue;      // a std::deque<int> inside
    plain.add(1);
    queue.add(2);
    queue.add(3);
    std::println("{} {}", plain.size(), queue.size());
}
One function, three containers Python · container_param_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra container_param.cpp -o container_param && ./container_param
g++-16 -std=c++23 -Wall -Wextra container_param.cpp -o container_param-gcc && ./container_param-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra container_param.cpp -o container_param && ./container_param
std::vector<int> [1, 4, 9, 16]
std::deque<int> [1, 4, 9, 16]
std::list<int> [1, 4, 9, 16]
1 2
$ g++-16 -std=c++23 -Wall -Wextra container_param.cpp -o container_param-gcc && ./container_param-gcc
std::vector<int> [1, 4, 9, 16]
std::deque<int> [1, 4, 9, 16]
std::__cxx11::list<int> [1, 4, 9, 16]
1 2

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 container_param_session.py

squares<std::vector>(4), squares<std::deque>(4) and squares<std::list>(4) passed the templates themselves, without arguments, and got three different container types holding the same four squares; C++23’s std::println printed each one as a range. Step through the calls and the two Inbox objects:

Interactive · step by step One template, given three containers Step through the calls. The left column is the template passed as an argument, with no arguments of its own; the right one is what squares builds from it.
One template, given three containers, step 5 of 5. Showing squares<std::vector>(4), squares<std::deque>(4), squares<std::list>(4), Container<int> (std::vector<int> [1, 4, 9, 16]), Container<int> (std::deque<int> [1, 4, 9, 16]), Container<int> (std::list<int> [1, 4, 9, 16]), Inbox<int> (sizes printed: 1 and 2). In focus: Inbox<int>.Container<int>Container<int>Container<int>squares<std::vector>(4)passes std::vectorsquares<std::deque>(4)passes std::dequesquares<std::list>(4)passes std::listContainer<int>std::vector<int> [1, 4, 9, 16]Container<int>std::deque<int> [1, 4, 9, 16]Container<int>std::list<int> [1, 4, 9, 16]Inbox<int>sizes printed: 1 and 2

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. A template as an argument. squares is declared template <template <typename...> class Container>: its parameter takes a class template whose parameters are all types. std::vector is passed as it is, with no arguments.
  2. The template supplies int. The body writes Container<int> out;, so for std::vector it builds a std::vector<int> and pushes 1, 4, 9 and 16 into it.
  3. The same body, a deque. squares<std::deque>(4) runs the same body with another template: out is a std::deque<int> holding the same four squares.
  4. And a list. squares<std::list>(4) gives a std::list<int>. Each call is a separate instantiation of squares, one per template passed in.
  5. A default, and one parameter. Inbox declares template <typename> class Container = std::vector, with one type parameter, and still accepts std::vector and std::deque, whose second parameter has a default: C++17 counts defaults when it matches. Inbox<int> held 1 item, the deque inbox 2.

Inbox declares its parameter as template <typename> class Container, with one type parameter, yet accepts std::vector and std::deque, which have two (the second is the allocator, with a default). That matching, where default arguments count, is a C++17 rule (paper P0522R0), implemented since GCC 7 and since Clang 19. With an older Clang, write template <typename...> or the full parameter list. Since C++17 the keyword in that position may also be typename instead of class.

Checkpoint

Check: Template template parameters

  1. Question 1 of 2 Which template parameter accepts std::vector itself, so that the template can write Container<int>?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: template <typename...> class Container

    A template template parameter takes a template, not a type. typename Container would accept std::vector<int>, which is already a complete type, but not std::vector on its own.

  2. Question 2 of 2 Inbox declares template <typename T, template <typename> class Container = std::vector>. Why does Inbox<int, std::deque> compile, although std::deque has two template parameters?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Since C++17 default arguments count when a template is matched, and the allocator parameter has one

    P0522R0 lets a template template argument match when it can be used with the parameter's argument list. The allocator's default makes std::deque<int> a valid use, so std::deque fits.

Practise: a RingBuffer with a compile-time capacity

ring_buffer.h declares RingBuffer<T, N>, a first-in, first-out queue that holds at most N elements in a std::array: a ring buffer: A fixed-size array used as a queue whose write position wraps from the last slot back to the first, so that once the array is full each new item replaces the oldest one. More in “C++ memory-bug hunt: ten broken programs”. push adds at the back and refuses when the buffer is full; pop takes the oldest element from the front. The member variables head_ (where the oldest element is) and count_ (how many there are) are ready for you; write push and pop.

The starter header and its sample tests

C header · ring_buffer.h

// Starter code for "Your turn": a first-in, first-out queue with room for exactly N elements.
#pragma once
#include <array>
#include <cstddef>
#include <optional>

template <typename T, std::size_t N>
class RingBuffer {
    static_assert(N > 0, "a RingBuffer holds at least one element");

public:
    // Adds value at the back. Returns false, and changes nothing, when the buffer is full.
    bool push(const T& value) {
        // TODO
        (void)value;
        return false;
    }

    // Removes the element at the front and returns it; std::nullopt when the buffer is empty.
    std::optional<T> pop() {
        // TODO
        return std::nullopt;
    }

    bool empty() const { return count_ == 0; }
    bool full() const { return count_ == N; }
    std::size_t size() const { return count_; }
    static constexpr std::size_t capacity() { return N; }

private:
    std::array<T, N> slots_{};
    std::size_t head_ = 0;   // index of the front element
    std::size_t count_ = 0;  // how many elements are stored
};

C++ · ring_buffer_tests.cpp

// Sample tests for ring_buffer.h. Build: clang++ -std=c++23 -Wall -Wextra ring_buffer_tests.cpp -o ring_buffer_tests
#include <print>
#include <string>
#include "ring_buffer.h"

int checks = 0;
int failures = 0;

void check(bool ok, const char* what) {
    ++checks;
    if (!ok) {
        ++failures;
        std::println("FAIL {}", what);
    }
}

int main() {
    RingBuffer<int, 3> q;
    check(q.empty() && !q.full() && q.capacity() == 3, "a new buffer is empty and holds three");
    check(q.push(1) && q.push(2) && q.push(3), "three pushes fit");
    check(q.full() && !q.push(4), "a fourth push is refused");
    check(q.pop() == 1 && q.pop() == 2, "pop() returns the oldest elements first");
    check(q.push(4) && q.push(5), "the freed slots are used again");
    check(q.pop() == 3 && q.pop() == 4 && q.pop() == 5, "the order survives the wrap-around");
    check(q.empty() && q.pop() == std::nullopt, "an empty buffer pops std::nullopt");

    RingBuffer<std::string, 1> one;
    check(one.push("only") && !one.push("more"), "a capacity of one holds one element");
    check(one.pop() == "only" && one.push("again") && one.size() == 1, "and can be refilled");

    static_assert(sizeof(RingBuffer<char, 64>) > sizeof(RingBuffer<char, 8>), "the capacity is part of the object");
    std::println("{} of {} checks passed", checks - failures, checks);
    return failures == 0 ? 0 : 1;
}
Two of nine checks pass with the starter Python · ring_buffer_session.py
from session import run

# Sample tests built against the starter: push() refuses everything and pop() finds nothing yet.
run("""
clang++ -std=c++23 -Wall -Wextra ring_buffer_tests.cpp -o ring_buffer_tests && ./ring_buffer_tests
g++-16 -std=c++23 -Wall -Wextra ring_buffer_tests.cpp -o ring_buffer_tests-gcc && ./ring_buffer_tests-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra ring_buffer_tests.cpp -o ring_buffer_tests && ./ring_buffer_tests
FAIL three pushes fit
FAIL a fourth push is refused
FAIL pop() returns the oldest elements first
FAIL the freed slots are used again
FAIL the order survives the wrap-around
FAIL a capacity of one holds one element
FAIL and can be refilled
2 of 9 checks passed
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra ring_buffer_tests.cpp -o ring_buffer_tests-gcc && ./ring_buffer_tests-gcc
FAIL three pushes fit
FAIL a fourth push is refused
FAIL pop() returns the oldest elements first
FAIL the freed slots are used again
FAIL the order survives the wrap-around
FAIL a capacity of one holds one element
FAIL and can be refilled
2 of 9 checks passed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ring_buffer_session.py

The two checks that pass already only look at a new, empty buffer. Indexes wrap around the end of the array: after pushes and pops, the newest element can sit in a slot before the oldest one. The diagram shows the tests’ RingBuffer<int, 3> in that state, after pushing 1, 2 and 3, popping twice and pushing 4:

A RingBuffer<int, 3> with head 2 and count 2: 4 in slot 0, slot 1 free, 3 in slot 2; pop takes 3, push writes slot 1.RingBuffer<int, 3>head = 2, count = 2pop() returnsslots_[head], 3,then head becomes(2 + 1) % 3 = 0push(x) writesslots_[(head + count) % 3],(2 + 2) % 3 = slot 1slot 04newestslot 1freeslot 23oldest

How a ring buffer's indexes wrap around

Text description of the diagram

The diagram shows the RingBuffer<int, 3> of the sample tests, a ring buffer whose three slots are a std::array inside the object, after these steps: push 1, 2 and 3, pop twice (1 and 2 come out), then push 4.

  1. The slots hold 4 in slot 0, nothing in slot 1 and 3 in slot 2. The newest element, 4, sits before the oldest one, 3, because the write position wrapped around past the last slot.
  2. The buffer keeps two numbers: head, the index of the oldest element, which is 2, and count, the number of stored elements, which is 2.
  3. pop() returns the element at head, 3, and moves head on by one, modulo the capacity: (2 + 1) % 3 = 0.
  4. push(x) writes to the slot (head + count) % 3, which is (2 + 2) % 3 = 1, the only free slot.

The same sequence of the tests runs below, every push and pop in order, with front for head_ and rear for the slot the next push writes, (head_ + count_) % N. Watch the fourth push being refused and the write position wrap from slot 2 to slot 0:

Interactive · step through it The sample tests' RingBuffer<int, 3>, push by push front and rear move round a fixed set of slots and wrap from the last slot to slot 0.
Circular queue of 3 slots: 0: empty, 1: empty, 2: empty. front 0, rear 0, 0 in use.012frontrearwraps around
  • being moved or written

Pseudo-code

  1. enqueue(x): if count = capacity: error "full"
  2. a[rear] ← x; rear ← (rear + 1) mod capacity; count ← count + 1
  3. dequeue(): if count = 0: error "empty"
  4. x ← a[front]; front ← (front + 1) mod capacity; count ← count − 1; return x

A circular queue with 3 slots. front is where the next item leaves, rear where the next one goes; both wrap around to slot 0 after slot 2.

When it finishes: 0 items queued.

Items 0
Operations 0

Compute the write position from head_ and count_ with % N, and move head_ forward the same way in pop. The tests include a buffer with a capacity of one, the case where an off-by-one error shows first. Then try a variation: when N is a power of two, i % N equals i & (N - 1), a mask: An integer with 1 bits exactly where an operation should act, such as 1u << 3 for bit 3. flags | mask sets those bits, flags & ~mask clears them, flags ^ mask flips them and flags & mask keeps only them. More in “C++ bitwise operators and the <bit> header” that is cheaper than a division. Because N is known at compile time, static_assert((N & (N - 1)) == 0) can insist on it.

Programmer Calculator (Bitwise) Type 7 and 13 in binary to see why 13 & 7 is 5, the same as 13 % 8, when the capacity is a power of two.

The questions below ask for capacities, sizes, deduced values and the templates a parameter accepts. The one you know least comes first, and each choice explains itself when you pick it:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 What does fixed_stack.cpp print?

    What does this program print? Choose one answer.

    // A stack whose capacity N is a template parameter: a value known at compile time, stored in the type.
    #include <array>
    #include <cstddef>
    #include <print>
    #include <vector>
    
    template <typename T, std::size_t N>
    class FixedStack {
        static_assert(N > 0, "a FixedStack holds at least one element");
    
    public:
        bool push(const T& value) {  // false when the stack is full
            if (count_ == N) return false;
            items_[count_++] = value;
            return true;
        }
        const T& top() const { return items_[count_ - 1]; }  // only for a non-empty stack
        std::size_t size() const { return count_; }
        static constexpr std::size_t capacity() { return N; }
    
    private:
        std::array<T, N> items_{};  // N elements inside the object: no heap allocation
        std::size_t count_ = 0;
    };
    
    int main() {
        FixedStack<int, 3> moves;
        for (int m : {7, 4, 9, 2}) {
            bool ok = moves.push(m);
            std::println("push({}) -> {}", m, ok ? "stored" : "full");
        }
        std::println("top {}, size {} of {}", moves.top(), moves.size(), moves.capacity());
    
        std::println("sizeof(FixedStack<int, 8>)     = {}", sizeof(FixedStack<int, 8>));
        std::println("sizeof(FixedStack<int, 16>)    = {}", sizeof(FixedStack<int, 16>));
        std::println("sizeof(FixedStack<double, 16>) = {}", sizeof(FixedStack<double, 16>));
        std::println("sizeof(std::vector<int>)       = {}", sizeof(std::vector<int>));  // its elements live elsewhere
    }
    Show the answer: Question 1 of 10

    Answer: it prints

    push(7) -> stored
    push(4) -> stored
    push(9) -> stored
    push(2) -> full
    top 9, size 3 of 3
    sizeof(FixedStack<int, 8>)     = 40
    sizeof(FixedStack<int, 16>)    = 72
    sizeof(FixedStack<double, 16>) = 136
    sizeof(std::vector<int>)       = 24

    The fourth push finds count_ == N and returns false. FixedStack<int, 8> holds 32 bytes of ints and an 8-byte count, 40; 16 ints make 72 and 16 doubles 136, while a std::vector<int> is 24 bytes wherever its elements are.

  2. Question 2 of 10 What happens with FixedStack<int, 0> s;?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: It does not compile: the static_assert N > 0 fails while compiling

    static_assert(N > 0, …) is evaluated when FixedStack<int, 0> is instantiated, so the error appears at compile time, with the message written in the class.

  3. Question 3 of 10 What did nm -C list for capacity_of in capacity_of.o?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: Two functions, capacity_of<int, 8ul> and capacity_of<int, 16ul>

    Each value of N made a separate instantiation, exactly as each type T did for larger. The suffix ul shows that the values are unsigned long, the type of std::size_t here.

  4. Question 4 of 10 A function must add up the elements of a Buffer<int, N> of any capacity, and must be compiled only once. What should it take?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: A std::span<const int> of the elements, whose length is a run-time value

    A template over N is right when code may differ per capacity; a view whose length is a run-time value, such as std::span, is the way to compile the work once.

  5. Question 5 of 10 Which types can a non-type template parameter have in C++20?

    Choose every answer that is right.

    Show the answer: Question 5 of 10

    Answer:

    • A class such as FixedString, whose members are all public
    • enum class Level
    • double

    Integers, enumerations, pointers and references have long been allowed; C++20 added floating-point types and structural class types, whose bases and members are all public and not mutable.

  6. Question 6 of 10 with_rate<0.18>(250.0) printed 295.00. What does with_rate<0.10>(250.0) return?

    Type a number.

    A hint

    The function returns amount * (1.0 + Rate).

    Show the answer: Question 6 of 10

    Answer: 275 (anything from 274.99 to 275.01 counts)

    250.0 * (1.0 + 0.10) is 275. Rate is a template argument, so with_rate<0.10> is a second function, compiled with 0.10 built in.

  7. Question 7 of 10 sizeof(price) was 8 for Field<"price", double> price. What is sizeof of a Field<"quantity", int> on this Mac?

    Type a number.

    Show the answer: Question 7 of 10

    Answer: 4 bytes

    A Field object holds only its value, here one 4-byte int. The name is part of the type, read by the static member function name(), so it takes no room in the object.

  8. Question 8 of 10 Are Field<"price", double> and Field<"Price", double> the same type?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: No: each text makes its own FixedString value, so its own type

    A class-type template argument is compared member by member. "price" and "Price" differ in their first character, so the two FixedString values differ, and so do the two Field types.

  9. Question 9 of 10 What does squares<std::deque>(4) return?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: A std::deque<int> holding 1, 4, 9 and 16

    squares<std::deque>(4) makes Container = std::deque, so out is a std::deque<int>, filled with the squares of 1 to 4, which the program printed as [1, 4, 9, 16].

  10. Question 10 of 10 Why does squares<std::vector<int>>(4) not compile?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: std::vector<int> is a type, and the parameter takes a template

    The kinds of parameter must match: a type parameter takes a type, a value parameter a value, and a template template parameter a template, written without arguments.

Interview questions

Fresher · Are RingBuffer<int, 8> and RingBuffer<int, 16> the same type? What follows from your answer?

No. A non-type template argument is part of the type, so each capacity makes a separate class, compiled separately, with its own member functions. Three things follow. An object of one cannot be assigned to or passed where the other is expected: a function taking RingBuffer<int, 8>& rejects the 16-slot buffer, so code that should work for any capacity has to be a template over N, or take a view such as std::span. The capacity costs nothing at run time: the object holds only the elements and the indexes, and the compiler can check the size with a static_assert. And every capacity the program uses adds its own copy of the code, so dozens of capacities make a larger binary than one run-time-sized container.

A follow-up usually checks that the answer carries over to new code. Try these two:

Checkpoint

Interview round: two follow-up questions

  1. Question 1 of 2 RingBuffer<char, 64> holds a std::array<char, 64> and two std::size_t members, head_ and count_. What is its sizeof on this 64-bit Mac?

    Type a number.

    A hint

    Each std::size_t is 8 bytes and must start at a multiple of 8.

    Show the answer: Question 1 of 2

    Answer: 80 bytes

    64 chars take 64 bytes, which is already a multiple of 8, so head_ and count_ follow without padding: 64 + 8 + 8 = 80. The capacity, 64, is in the type and takes no byte of its own.

  2. Question 2 of 2 The interviewer wants one print that takes a RingBuffer<int, N> of any capacity. Which declaration?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: template <std::size_t N> void print(const RingBuffer<int, N>& r);

    Code that should take every capacity is a template over N. Deduction reads N from the type of the argument, so a call such as print(q) needs no angle brackets.

Two harder questions test the design trade-offs: a capacity that must come from a file at start-up, and field names carried in types:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    The capacity of a message queue declared RingBuffer<Message, N> must now come from a configuration file read at start-up. Why can't the value from the file be N, and how would you redesign the buffer so that both compile-time and run-time capacities work without writing the logic twice?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    You want Field<"price", double> so that each field's name is part of its type. Why does template <const char* Name> refuse "price", how does a C++20 FixedString parameter make it work, and what does putting names into types cost?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A non-type template parameter is a value known at compile time, such as N in std::array<T, N>; it can size arrays inside the object and be checked with static_assert.
  • Each value makes a different type: FixedStack<int, 8> is 40 bytes and FixedStack<int, 16> 72, they do not convert to each other, and function names carry the values (capacity_of<int, 8ul>).
  • Function templates can deduce a value from an argument’s type, including the length of an array passed by reference.
  • template <auto V> (C++17) deduces the value’s type; C++20 adds floating-point values, which match only when identical bit for bit, and structural class types.
  • A string literal cannot be a pointer argument, but a C++20 FixedString class parameter can hold it, making names part of types without adding a byte to the objects.
  • template <typename...> class Container accepts templates such as std::vector; since C++17 default arguments let template <typename> class match them too.

Terms in this lesson

non-type template parameter
A template parameter that holds a value instead of a type, such as N in template <typename T, std::size_t N>. Its argument must be a constant expression, and each value makes a different type: std::array<int, 5> is not std::array<int, 6>.
structural type
A type that may be the type of a non-type template parameter: a scalar type such as int, double or a pointer, an lvalue reference, or (C++20) a literal class whose bases and non-static data members are all public, not mutable and structural themselves.
template template parameter
A template parameter that takes a template, such as template <typename...> class Container, so that a call can pass std::vector itself and the template writes Container<int> with arguments of its own choosing.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.