Skip to content
BoKSA

Templates and Generic Code

Templates and Generic Code

Introduction

In C you wrote int_max and float_max, or abused macros. Templates let you write one function or class that works for many types — the compiler generates the versions you actually use. That is powerful for ring buffers, filters, and containers; it also increases flash if you instantiate many types.

This article explains how templates work, when they help embedded code, and when to stick with plain C or a single typed implementation.


Function templates — one pattern, many types

1
2
3
4
5
6
7
template<typename T>
T max_value(T a, T b) {
    return (a > b) ? a : b;
}

int m1 = max_value(3, 7);
float m2 = max_value(2.5f, 1.1f);

The compiler generates max_value<int> and max_value<float> — two functions. You write once; the compiler specializes.

In plain terms

A template is a cookie cutter. You draw one shape; the machine stamps dough for each type you request. Each stamp uses flash — only stamp shapes you need.


Class templates — generic containers

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
template<typename T, size_t N>
class RingBuffer {
public:
    bool push(const T &item);
    bool pop(T &item);
    bool empty() const { return count_ == 0; }

private:
    T data_[N];
    size_t head_ = 0, tail_ = 0, count_ = 0;
};

RingBuffer<uint16_t, 128> adc_samples;
RingBuffer<uint8_t, 64>  uart_rx;

One implementation — two buffer types. Size N is a template parameter — often known at compile time (no heap).

Compare Data structures in C: same FIFO idea, type-safe without void *.


Template parameters — type vs value

1
2
3
template<typename T>           /* type parameter */
template<int N>                /* non-type parameter */
template<typename T, size_t N> /* both */

Non-type parameters are ideal for fixed buffer sizes on MCU — stack or static arrays inside the class, visible to optimizer.


constexpr — compile-time constants

1
2
3
4
5
6
7
constexpr int baud_default = 115200;
constexpr size_t buffer_size = 256;

template<size_t N>
struct Buffer { uint8_t data[N]; };

Buffer<buffer_size> tx;

constexpr means "computable at compile time" (C++11+). Use for pin masks, table sizes, and static checks — zero runtime cost when used in constant contexts.


Code size trade-off

Each template instantiation is separate machine code:

1
2
3
max_value(int, int);
max_value(float, float);
max_value(double, double);  /* third copy in flash */
Strategy When
Template on one type you need Sensor pipeline all int16_t
Explicit instantiation in one .cpp Control .o size
Non-template C function Shared library, many types via void *
#if / type alias Only two variants in product

Always size firmware.elf after adding templates.


inline and headers

Template definitions usually live in headers — compiler needs full body to instantiate:

1
2
3
4
5
// ring_buffer.hpp
template<typename T, size_t N>
bool RingBuffer<T, N>::push(const T &item) {
  /* ... */
}

Every .cpp that includes the header can generate code — link one explicit instantiation if needed, or keep template entirely in header for small functions.


SFINAE and concepts — awareness only

Advanced metaprogramming (enable_if, C++20 concepts) filters which types can use a template. Robotics code on MCU rarely needs this in year 2–3 — know it exists when library errors look like novel length.


Templates vs macros

Macro Template
Type check No Yes
Debugger Poor Better
Code bloat Text paste Per instantiation
Suitable for Register bit masks Algorithms, buffers

Prefer templates over function-like macros for min/max/clamp in C++.


Embedded examples

  • Fixed ring buffer for UART DMA (template N)
  • Moving average filter template<typename T, int Window>
  • std::array<T, N> instead of T arr[N] with size in type
  • Eigen / etl::vector on larger targets — not on 32 KB RAM MCU

Relevant topics


Starting points

  1. Implement template<typename T> T clamp(T v, T lo, T hi) — instantiate for int and float; compare .elf size.
  2. Replace a #define MAX(a,b) with template max_value.
  3. Use std::array<uint8_t, 64> in a hosted test build.
  4. Count instantiations in map file if linker supports it.

Focus points

  • Templates live in headers unless explicitly instantiated.
  • Flash cost scales with instantiations — not free abstraction.
  • Keep template parameters small on MCU — prefer uint16_t pipeline over generic everything.
  • Readable namestemplate<typename T> is fine; typename SampleType clearer in APIs.

Key points

  • Templates generate type-specific code at compile time from one pattern.
  • Class templates build generic buffers and containers with compile-time sizes.
  • constexpr moves computation to compile time when possible.
  • On embedded, measure flash and prefer templates only where duplication pays off.