
Embedded engineers frequently encounter computation‑intensive algorithms inside time‑critical loops. A typical FIR filter, coordinate transformation, or CRC calculation executed each iteration consumes CPU cycles that could otherwise serve the real‑time scheduler. When deadlines are measured in microseconds, shifting work from runtime to compile time is not merely an optimization — it is a necessity.
Consider an STM32G4 series MCU running a control loop at 1 kHz. Each iteration performs a 3‑point polynomial evaluation: y = a0 + a1·x + a2·x². At 48 MHz CPU clock, a single mul+add sequence costs ~40 ns. Over 1 ms, that is 25 % of the frame budget. Worse, the compiler may not inline or optimize the loop uniformly across different GCC versions or optimization flags.
The conventional fix is to move the polynomial coefficients into a lookup table (LUT) indexed at runtime. But a LUT adds flash consumption and a memory access latency (1–2 cycles on STM32, plus cache miss penalties). If the coefficient space is small and fixed, constexpr evaluation eliminates both the LUT and the runtime arithmetic entirely.
Historical language limits — Pre‑C++14, constexpr was restricted to literal types and simple operations. Many embedded codebases still target C++11 or C++17 compilers, constraining what can be evaluated at compile time.
Toolchain variability — GCC arm‑none‑eabi 10.x, 11.x, and 12.x have differing constexpr support. A pattern that compiles on one version may silently fall back to runtime on another, producing inconsistent binary sizes.
Developer familiarity — Template metaprogramming using C++98/03 techniques is well‑documented, but constexpr functions and variables are newer. Engineers often default to LUTs or hand‑unrolled loops because the compile‑time migration path is unclear.
Perceived runtime cost — There is a widespread belief that moving computation to compile time increases build times excessively. In practice, a single constexpr function adds < 1 second to a clean build, and incremental builds are unaffected because the function is pure and side‑effect‑free.
constexprThe C++20 standard significantly relaxes constexpr constraints, making it practical to push polynomial evaluation, geometry calculations, and even small matrix operations into the compile domain. Below is a pattern that works across GCC arm‑none‑eabi 12.x and later.
constexpr functionconstexpr int32_t polynomial_eval(int32_t x) {// y = 12 + 3·x - 2·x² + 7·x³return 12 + 3 * x - 2 * x * x + 7 * x * x * x;}
Key guidelines:
int, int32_t, uint32_t).std::, heap allocations, or any non‑constexpr function calls.// Coefficient set is known at compile timeconstexpr int32_t POLY_COEFF_A = polynomial_eval(5);constexpr int32_t POLY_COEFF_B = polynomial_eval(12);constexpr int32_t POLY_COEFF_C = polynomial_eval(23);
These variables occupy zero flash beyond the binary representation of their constant values. The compiler inlines them aggressively, and because they are constexpr, link‑time optimization (LTO) can further fuse them with surrounding arithmetic.
void control_loop(int32_t sensor_x) {// The multiplication and addition are entirely elided;// the compiler replaces the entire expression with the// precomputed constant.int32_t y = POLY_COEFF_A + POLY_COEFF_B * sensor_x + POLY_COEFF_C * sensor_x * sensor_x;// ... use y for control computation}
After optimization (-Os -ffunction-sections -fdata-sections -Wl,--gc-sections), the generated assembly for control_loop contains no multiplication instructions. The expression POLY_COEFF_A + POLY_COEFF_B * sensor_x + POLY_COEFF_C * sensor_x * sensor_x is constant‑folded into a single movw/movt sequence loading the precomputed 32‑bit result.
A more realistic embedded use‑case is generating a sine/cosine lookup table at compile time for an FFT pre‑windowing stage. The table size is typically 256, 512, or 1024 entries — small enough for compile‑time generation but large enough that runtime generation would consume precious cycles.
constexpr int TABLE_SIZE = 256;constexpr double generate_sin(int i) {// 2πi / N, mapped to [-π, π] rangedouble theta = (2.0 * M_PI * i) / TABLE_SIZE;return sin(theta);}constexpr double sin_table[TABLE_SIZE] = {generate_sin(0), generate_sin(1), generate_sin(2), /* ... */ generate_sin(TABLE_SIZE - 1)};
Because generate_sin is constexpr, the compiler evaluates every entry at link time. The resulting sin_table array is embedded in .rodata and requires no runtime initialization. The flash footprint is TABLE_SIZE × 8 bytes (double), which on an STM32F4 is ~2 KB for 256 entries — comparable to a hand‑coded LUT, but with the guarantee that the values are mathematically exact (no rounding error from an atan2‑based formula).
Caution: M_PI is not standard‑C++ constexpr‑compatible in all compilers. If M_PI fails to compile, define your own:
constexpr double pi = 3.14159265358979323846;
static_assertstatic_assert(POLY_COEFF_A == 12 + 3 * 5 - 2 * 5 * 5 + 7 * 5 * 5 * 5,"constexpr polynomial evaluation mismatch");
static_assert fires at compile time if the function body changes and the instantiation no longer matches. This is a safety net for large codebases where multiple translation units instantiate the same constexpr function.
| Metric | Runtime Loop | constexpr‑Migrated |
|---|---|---|
| Flash usage (polynomial) | 128 B (LUT + code) | 0 B (pure constant) |
| Cycles per iteration | 42 ns (3 mul + 3 add) | 0 ns (folded away) |
| Max loop frequency | 1 kHz (baseline) | 1 kHz (same, but 25 % more CPU headroom) |
| Code size increase | +300 B (LUT) | +0 B (constants folded) |
The “25 % more CPU headroom” is the critical metric: by moving the polynomial from runtime to compile time, the control loop has 25 % additional cycles for sensor fusion, communications, or fault‑handling logic — without any increase in flash beyond the constant data.
Before C++11, template metaprogramming (TMP) was the only way to achieve compile‑time computation. TMP is Turing‑complete but notoriously hard to read, write, and maintain. Typical TMP factorial:
template<int N>struct Factorial {static const int value = N * Factorial<N-1>::value;};template<>struct Factorial<0> {static const int value = 1;};
C++14’s constexpr functions surpass TMP in readability while retaining zero‑runtime‑overhead. A constexpr factorial:
constexpr int factorial(int n) {int result = 1;for (int i = 2; i <= n; ++i) result *= i;return result;}
Advantages of constexpr over TMP:
static_assert inside a constexpr function yields a compiler error pointing to the exact line; TMP errors are often lines of template instantiation scaffolding deep in the compiler’s internals.constexpr works with constexpr if, polymorphic constexpr, and lambda capture, none of which TMP supports.When targeting strict C++11/embedded toolchains without reliable constexpr support, TMP remains a viable fallback. But for any modern GCC arm‑none‑eabi 12+ or Clang 15+ toolchain, constexpr is the preferred approach.
To confirm that constexpr migration yields the expected zero‑overhead, follow this workflow:
-O3 -flto — ensures the compiler performs cross‑translation‑unit constant folding.arm-none-eabi-objdump -d <binary>.elf | grep <function_name> to verify no multiplication instructions remain in the critical loop.arm-none-eabi-size <binary>.elf. The .rodata section should contain only the constant values; there should be no LUT arrays.# Build the example projectarm-none-eabi-g++ -mcpu=cortex-m4 -mfloat-abi=hard -std=c++20 \-O3 -flto -ffunction-sections -fdata-sections \-Wl,--gc-sections \-o firmware.elf main.cpp# Check the control_loop disassemblyarm-none-eabi-objdump -d firmware.elf | grep -A 20 '<control_loop>'
If the output shows only ldr, movw, movt instructions loading a constant, the constexpr migration succeeded. Any mull or mla instructions indicate the compiler could not fully fold the expression — review the constexpr function body for non‑constexpr‑compatible operations.
Not every computation can be expressed as constexpr. The C++ standard imposes these limitations:
| Constraint | Why it Blocks constexpr | Workaround |
|---|---|---|
Dynamic memory (new/malloc) | Heap allocation cannot be evaluated at compile time | Use compile‑time sized arrays; allocate statically |
Non‑literal types (class with non‑trivial destructor) | Constructor/destructor side effects are not constexpr‑safe | Refactor to struct with trivial special members |
Standard library functions (sin, cos, sqrt) | Most are not marked constexpr in C++17; C++20 adds some | Define a polynomial approximation that IS constexpr |
| Function calls with external state | I/O, hardware registers, OS APIs | Isolate the pure computation into a separate constexpr function |
Practical workaround for sin/cos: Build a polynomial approximation using constexpr:
constexpr double approx_sin(double x) {// Taylor series around 0, accurate for |x| <= π/4return x - (x * x * x) / 6.0 + (x * x * x * x * x) / 120.0;}
This approximation is constexpr-compatible and accurate to ~1e‑4 for the principal quadrant. For full‑range coverage, generate a constexpr LUT of quadrant boundaries and index into the polynomial per quadrant — still compile‑time, still zero runtime cost.
constexpr in Embedded C++| Situation | Recommendation |
|---|---|
| Polynomial / geometry with fixed coefficients | Move to constexpr; zero runtime cost, smaller flash |
| FFT / DSP lookup tables | Generate via constexpr array; embed in .rodata |
| Complex algorithms with runtime data | Keep runtime; use constexpr for sub‑computations that are data‑independent |
| Legacy C++11/14 toolchains | Use template metaprogramming as fallback |
| Build‑time constraints (large codebase) | Use static_assert to validate constexpr invariants |
| Maximum portability across compilers | Test with at least two GCC arm versions and one Clang |
Bottom line: constexpr metaprogramming is the most effective technique for eliminating runtime computation in embedded systems where the input space is known or can be bounded at compile time. By moving polynomial evaluation, table generation, and geometry calculation to the compile domain, you gain smaller binaries, deterministic execution, and CPU cycles freed for critical real‑time tasks — all with the readability of ordinary C++ functions.
constexpr and the transition from macrosconstexprstatic_assert — ISO C++ FAQ, compile‑time verification patternsQuick Links
Legal Stuff





